From 622d15593aba536625bebfe84230d171252e303f Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 4 Dec 2024 20:36:37 +0000 Subject: [PATCH 01/41] change PostTypes to abstract class --- src/Contracts/PostTypeContract.php | 79 +++++++ src/PostType.php | 340 +++++------------------------ 2 files changed, 136 insertions(+), 283 deletions(-) create mode 100644 src/Contracts/PostTypeContract.php diff --git a/src/Contracts/PostTypeContract.php b/src/Contracts/PostTypeContract.php new file mode 100644 index 0000000..1e4ae28 --- /dev/null +++ b/src/Contracts/PostTypeContract.php @@ -0,0 +1,79 @@ +names($names); - - // assign custom options to the PostType - $this->options($options); - - // assign labels to the PostType - $this->labels($labels); + return $this->name(); } /** - * Set the names for the PostType - * @param mixed $names A string for the name, or an array of names - * @return $this + * Post type labels. + * + * @return array */ - public function names($names) + public function labels(): array { - // 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; + return []; } /** - * Set the options for the PostType - * @param array $options An array of options for the PostType - * @return $this + * Post type options. + * + * @return array */ - public function options(array $options) + public function options(): array { - $this->options = $options; - - return $this; + return []; } /** - * Set the labels for the PostType - * @param array $labels An array of labels for the PostType - * @return $this + * Post type taxonomies. + * + * @return array */ - public function labels(array $labels) + public function taxonomies(): array { - $this->labels = $labels; - - return $this; + return []; } /** - * 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 + * Post type supports. + * + * @return array */ - public function filters(array $filters) + public function supports(): array { - $this->filters = $filters; - - return $this; + return [ + 'title', + 'editor', + ]; } /** - * Set the menu icon for the PostType - * @param string $icon A dashicon class for the menu icon - * @return $this + * Post type icon. + * + * @return string|null */ - public function icon($icon) + public function icon(): ?string { - $this->icon = $icon; - - return $this; + return null; } /** - * Flush rewrite rules - * @link https://codex.wordpress.org/Function_Reference/flush_rewrite_rules - * @param boolean $hard - * @return void + * Post type filters. + * + * @return array */ - public function flush($hard = true) + public function filters(): array { - flush_rewrite_rules($hard); + return []; } /** - * Get the Column Manager for the PostType + * Post type columns. + * + * @param Columns $columns * @return Columns */ - public function columns() + public function columns(Columns $columns): Columns { - if (!isset($this->columns)) { - $this->columns = new Columns; - } - - return $this->columns; + return $columns; } /** - * Register the PostType to WordPress + * Post type additional hooks. + * * @return void */ - public function register() + public function hooks(): void { - (new PostTypeRegistrar($this))->register(); + return; } /** - * Create the required names for the PostType + * Register the post type. + * * @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' => "Add New", - 'add_new_item' => "Add New {$this->singular}", - 'edit_item' => "Edit {$this->singular}", - 'new_item' => "New {$this->singular}", - 'view_item' => "View {$this->singular}", - 'search_items' => "Search {$this->plural}", - 'not_found' => "No {$this->plural} found", - 'not_found_in_trash' => "No {$this->plural} found in Trash", - 'parent_item_colon' => "Parent {$this->singular}:", - ]; - - return array_replace_recursive($labels, $this->labels); - } - - /** - * Calculate the filters for the PostType - * @return array - */ - public function getFilters() + public function register(): void { - // 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; + (new PostTypeRegistrar($this))->register(); } } From cc81d97319cde853351ada523b267a34b274a89e Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 4 Dec 2024 21:11:51 +0000 Subject: [PATCH 02/41] update PostTypeRegistrar to accept new PostType --- src/Registrars/PostTypeRegistrar.php | 264 +++++++++--------- tests/PostTypeTest.php | 297 +-------------------- tests/Registrars/PostTypeRegistrarTest.php | 18 +- 3 files changed, 127 insertions(+), 452 deletions(-) diff --git a/src/Registrars/PostTypeRegistrar.php b/src/Registrars/PostTypeRegistrar.php index 898e41f..73db695 100644 --- a/src/Registrars/PostTypeRegistrar.php +++ b/src/Registrars/PostTypeRegistrar.php @@ -2,220 +2,206 @@ namespace PostTypes\Registrars; -use PostTypes\PostType; +use PostTypes\Contracts\PostTypeContract; +use PostTypes\Columns; class PostTypeRegistrar { - protected $posttype; + /** + * PostType to register. + * + * @var PostTypeContract + */ + private $posttype; + + /** + * The PostType columns. + * + * @var Columns + */ + private $columns; - public function __construct(PostType $posttype) + /** + * Constructor. + * + * @param PostTypeContract $posttype + */ + public function __construct(PostTypeContract $posttype) { $this->posttype = $posttype; + + $this->columns = $posttype->columns(new Columns()); } + /** + * Register the PostType to WordPress. + * + * @return void + */ public function register() { - // Get the PostType name. - $name = $this->posttype->name; + $name = $this->posttype->name(); - // Register the PostType - if (!post_type_exists($name)) { - add_action('init', [$this, 'registerPostType'], 10); - } else { + if (post_type_exists($name)) { + // Modify the existing PostType if it exists. add_filter('register_post_type_args', [$this, 'modifyPostType'], 10, 2); + } else { + // Register the new PostType to WordPress. + add_action('init', [$this, 'registerPostType'], 10, 0); } - // register Taxonomies to the PostType - add_action('init', [$this, 'registerTaxonomies'], 10); - - // modify filters on the admin edit screen - add_action('restrict_manage_posts', [$this, 'modifyFilters'], 10, 1); - - if (isset($this->posttype->columns)) { - // modify the admin edit columns. - add_filter('manage_' . $name . '_posts_columns', [$this, 'modifyColumns'], 10, 1); - - // populate custom columns - add_filter('manage_' . $name . '_posts_custom_column', [$this, 'populateColumns'], 10, 2); + // Handle PostType filters. + add_action('restrict_manage_posts', [$this, 'modifyFilters'], 10, 2); - // run filter to make columns sortable. - add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); + // Handle PostType columns. + add_filter('manage_' . $name . '_posts_columns', [$this, 'modifyColumns'], 10, 1); + add_filter('manage_' . $name . '_posts_custom_column', [$this, 'populateColumns'], 10, 2); + add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); + add_action('pre_get_posts', [$this, 'sortSortableColumns'], 10, 1); - // run action that sorts columns on request. - add_action('pre_get_posts', [$this, 'sortSortableColumns'], 10, 1); - } + // Register custom hooks. + $this->posttype->hooks(); } /** - * Register the PostType + * Register the PostType. + * * @return void */ public function registerPostType() { - // create options for the PostType - $options = $this->posttype->createOptions(); - - // check that the post type doesn't already exist - if (!post_type_exists($this->posttype->name)) { - // register the post type - register_post_type($this->posttype->name, $options); - } + register_post_type($this->posttype->name(), $this->generateOptions()); } /** - * Modify the existing Post Type. + * Modify the existing PostType. * + * @param array $args + * @param string $posttype * @return array */ public function modifyPostType(array $args, string $posttype) { - if ($posttype !== $this->posttype->name) { + if ($posttype !== $this->posttype->name()) { return $args; } - // create options for the PostType - $options = $this->posttype->createOptions(); + // create options for the PostType. + $options = $this->generateOptions(); return array_replace_recursive($args, $options); } /** - * Register Taxonomies to the PostType - * @return void + * Generate the options for the PostType. + * + * @return array */ - public function registerTaxonomies() + public function generateOptions() { - if (empty($this->posttype->taxonomies)) { - return; - } - - foreach ($this->posttype->taxonomies as $taxonomy) { - register_taxonomy_for_object_type($taxonomy, $this->posttype->name); - } + $defaults = [ + 'public' => true, + 'show_in_rest' => true, + 'labels' => $this->posttype->labels(), + 'taxonomies' => $this->posttype->taxonomies(), + 'supports' => $this->posttype->supports(), + 'menu_icon' => $this->posttype->icon(), + 'rewrite' => [ + 'slug' => $this->posttype->slug(), + ], + ]; + + return array_replace_recursive($defaults, $this->posttype->options()); } /** - * Modify and display filters on the admin edit screen - * @param string $posttype The current screen post type + * Modify the PostType filters. + * + * @param string $posttype * @return void */ public function modifyFilters($posttype) { - // first check we are working with the this PostType - if ($posttype === $this->posttype->name) { - // calculate what filters to add - $filters = $this->posttype->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->posttype->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. - sprintf( - '', - $taxonomy, - $tax->labels->filter_by_item - ); - - // Output dropdown for taxonomy. - wp_dropdown_categories($dropdown_args); + if ($posttype !== $this->posttype->name()) { + return; + } + + foreach ($this->posttype->filters() as $taxonomy) { + if (!is_object_in_taxonomy($posttype, $taxonomy)) { + continue; } + + $query_var = get_taxonomy($taxonomy)->query_var; + $selected = isset($_GET[$query_var]) ? $_GET[$query_var] : ''; + + $options = [ + 'name' => $query_var, //$taxonomy, + 'value_field' => 'slug', + 'taxonomy' => $taxonomy, + 'show_option_all' => get_taxonomy($taxonomy)->labels->all_items, + 'hierarchical' => get_taxonomy($taxonomy)->hierarchical, + 'hide_empty' => 0, + 'show_count' => 0, + 'orderby' => 'name', + 'selected' => $selected, //isset($_GET[$taxonomy]) ? $_GET[$taxonomy] : '', + ]; + + echo ''; + + wp_dropdown_categories($options); } } /** - * Modify the columns for the PostType - * @param array $columns Default WordPress columns - * @return array The modified columns + * Modify the PostType columns. + * + * @param array $columns + * @return array */ - public function modifyColumns($columns) + public function modifyColumns(array $columns) { - return $this->posttype->columns->modifyColumns($columns); + return $this->columns->applyColumns($columns); } + /** - * Populate custom columns for the PostType - * @param string $column The column slug - * @param int $post_id The post ID + * Populate the PostType columns. + * + * @param string $column + * @param int $post_id + * @return void */ public function populateColumns($column, $post_id) { - if (isset($this->posttype->columns->populate[$column])) { - call_user_func_array($this->posttype->columns()->populate[$column], [$column, $post_id]); - } + $this->columns->populateColumn($column, [$post_id]); } /** - * Make custom columns sortable - * @param array $columns Default WordPress sortable columns + * Set the PostTypes sortable columns. + * + * @param array $columns + * @return array */ public function setSortableColumns($columns) { - if (!empty($this->posttype->columns()->sortable)) { - $columns = array_merge($columns, $this->posttype->columns()->sortable); - } - - return $columns; + return $this->columns->setSortable($columns); } /** - * Set query to sort custom columns - * @param WP_Query $query + * Sort PostType columns. + * + * @param \WP_Query $query + * @return void */ 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->posttype->name) { + if (!is_admin() || !$query->is_main_query()) { return; } - $orderby = $query->get('orderby'); - - // if the sorting a custom column - if ($this->posttype->columns()->isSortable($orderby)) { - // get the custom column options - $meta = $this->posttype->columns()->sortableMeta($orderby); + $column = $query->get('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); - } + $this->columns->sortColumn($column, $query); } } diff --git a/tests/PostTypeTest.php b/tests/PostTypeTest.php index 2e3bd91..891d322 100644 --- a/tests/PostTypeTest.php +++ b/tests/PostTypeTest.php @@ -1,303 +1,8 @@ books = new PostType('book'); - } - - /** @test */ - public function canCreatePostType() - { - $this->assertInstanceOf(PostType::class, $this->books); - } - - /** @test */ - public function hasNameOnInstantiation() - { - $this->assertEquals($this->books->names['name'], 'book'); - } - - /** @test */ - public function hasNamesOnInstantiation() - { - $names = [ - 'name' => 'book', - 'singular' => 'Book', - 'plural' => 'Books', - 'slug' => 'books' - ]; - - $books = new PostType($names); - - $this->assertEquals($books->names, $names); - } - - /** @test */ - public function hasOptionsOnInstantiation() - { - $this->assertEquals($this->books->options, []); - } - - /** @test */ - public function hasCustomOptionsOnInstantiation() - { - $options = [ - 'public' => true - ]; - - $books = new PostType('books', $options); - - $this->assertEquals($books->options, $options); - } - - /** @test */ - public function hasLabelsOnInstantiation() - { - $this->assertEquals($this->books->labels, []); - } - - /** @test */ - public function hasCustomLabelsOnInstantiation() - { - $labels = [ - 'name' => 'Books', - 'add_new' => 'Add New Book' - ]; - - $books = new PostType('books', [], $labels); - - $this->assertEquals($books->labels, $labels); - } - - /** @test */ - public function taxonomiesEmptyOnInstantiation() - { - $this->assertEquals($this->books->taxonomies, []); - } - - /** @test */ - public function hasCustomTaxonomiesWhenPassed() - { - $books = $this->books; - - $books->taxonomy('genre'); - - $this->assertEquals($books->taxonomies, ['genre']); - } - - /** @test */ - public function canAddMultipleTaxonomies() - { - $books = $this->books; - - $books->taxonomy(['genre', 'publisher']); - - $this->assertEquals($books->taxonomies, ['genre', 'publisher']); - } - - /** @test */ - public function filtersNullOnInstantiation() - { - $this->assertNull($this->books->filters); - } - - /** @test */ - public function hasFiltersWhenAdded() - { - $books = $this->books; - - $books->filters(['genre']); - - $this->assertEquals($books->filters, ['genre']); - } - - /** @test */ - public function iconNullOnInstantiation() - { - $this->assertNull($this->books->icon); - } - - /** @test */ - public function hasIconWhenSet() - { - $books = $this->books; - - $books->icon('dashicon-book-alt'); - - $this->assertEquals($books->icon, 'dashicon-book-alt'); - } - - /** @test */ - public function columnsIsNullOnInstantiation() - { - $this->assertEquals($this->books->columns, null); - } - - /** @test */ - public function columnsReturnsInstanceOfColumns() - { - $this->assertInstanceOf(Columns::class, $this->books->columns()); - } - - /** @test */ - public function namesCreatedFromName() - { - $this->books->createNames(); - - $this->assertEquals($this->books->name, 'book'); - $this->assertEquals($this->books->singular, 'Book'); - $this->assertEquals($this->books->plural, 'Books'); - $this->assertEquals($this->books->slug, 'books'); - } - - /** @test */ - public function smartNamesCreatedFromName() - { - $story = new PostType('story'); - - $this->assertEquals($story->name, 'story'); - $this->assertEquals($story->singular, 'Story'); - $this->assertEquals($story->plural, 'Stories'); - $this->assertEquals($story->slug, 'stories'); - } - - /** @test */ - public function passedNamesAreUsed() - { - $names = [ - 'name' => 'book', - 'singular' => 'Single Book', - 'plural' => 'Multiple Books', - 'slug' => 'slug_books', - ]; - - $this->books->names($names); - - $this->books->createNames(); - - $this->assertEquals($this->books->name, 'book'); - $this->assertEquals($this->books->singular, 'Single Book'); - $this->assertEquals($this->books->plural, 'Multiple Books'); - $this->assertEquals($this->books->slug, 'slug_books'); - } - - /** @test */ - public function defaultOptionsUsedIfNotSet() - { - // generated options - $options = $this->books->createOptions(); - - // expected options - $defaults = [ - 'public' => true, - 'labels' => $this->books->createLabels(), - 'rewrite' => [ - 'slug' => $this->books->slug - ] - ]; - - $this->assertEquals($options, $defaults); - } - - /** @test */ - public function optionsGeneratedCorrectly() - { - // Set custom options - $this->books->options([ - 'public' => false, - ]); - - // Set option with helper method - $this->books->icon('dashicon-book-alt'); - - // generated options - $options = $this->books->createOptions(); - - // expected options - $expected = [ - 'public' => false, - 'labels' => $this->books->createLabels(), - 'menu_icon' => $this->books->icon, - 'rewrite' => [ - 'slug' => $this->books->slug - ] - ]; - - $this->assertEquals($options, $expected); - } - - /** @test */ - public function defaultLabelsAreGenerated() - { - $labels = $this->books->createLabels(); - - $defaults = [ - 'name' => $this->books->plural, - 'singular_name' => $this->books->singular, - 'menu_name' => $this->books->plural, - 'all_items' => $this->books->plural, - 'add_new' => "Add New", - 'add_new_item' => "Add New {$this->books->singular}", - 'edit_item' => "Edit {$this->books->singular}", - 'new_item' => "New {$this->books->singular}", - 'view_item' => "View {$this->books->singular}", - 'search_items' => "Search {$this->books->plural}", - 'not_found' => "No {$this->books->plural} found", - 'not_found_in_trash' => "No {$this->books->plural} found in Trash", - 'parent_item_colon' => "Parent {$this->books->singular}:", - ]; - - $this->assertEquals($labels, $defaults); - } - - /** @test */ - public function filtersAreEmptyIfNotSetAndNoTaxonomies() - { - $filters = $this->books->getFilters(); - - $this->assertEquals($filters, []); - } - - /** @test */ - public function filtersAreSameAsTaxonomyIfNotSet() - { - $this->books->taxonomy('genre'); - - $filters = $this->books->getFilters(); - - $this->assertEquals($filters, ['genre']); - } - - /** @test */ - public function filtersAreWhatAssignedIfPassed() - { - $this->books->filters(['genre', 'published']); - - $this->books->taxonomy('genre'); - - $filters = $this->books->getFilters(); - - $this->assertEquals($filters, ['genre', 'published']); - } - - /** @test */ - public function filtersAreEmptyIfSetWithEmptyArray() - { - $this->books->filters([]); - - $this->books->taxonomy('genre'); - - $filters = $this->books->getFilters(); - - $this->assertEquals($filters, []); - } + // } diff --git a/tests/Registrars/PostTypeRegistrarTest.php b/tests/Registrars/PostTypeRegistrarTest.php index e6a64bd..1ba653b 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -1,24 +1,8 @@ false, - ]); - - $registrar = new PostTypeRegistrar($posttype); - - $options = $registrar->modifyPostType([ - 'public' => true, - ], 'post'); - - $this->assertEquals(false, $options['public']); - } + // } From fd3a79887fddf452cff2320ee7972b81eab7da72 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 4 Dec 2024 21:14:29 +0000 Subject: [PATCH 03/41] create Columns and create Column abstract --- src/Column.php | 78 +++++++++ src/Columns.php | 269 ++++++++++++++----------------- src/Contracts/ColumnContract.php | 58 +++++++ tests/ColumnsTest.php | 267 ++++++++++-------------------- 4 files changed, 337 insertions(+), 335 deletions(-) create mode 100644 src/Column.php create mode 100644 src/Contracts/ColumnContract.php diff --git a/src/Column.php b/src/Column.php new file mode 100644 index 0000000..6db2b1c --- /dev/null +++ b/src/Column.php @@ -0,0 +1,78 @@ +name())); + } + + /** + * Populate the column. + * + * @param integer $objectId + * @return void + */ + public function populate(int $objectId): void + { + return; + } + + /** + * Set the column order. + * + * @return integer|null + */ + public function order(): ?int + { + return null; + } + + /** + * Handle sorting the column. + * + * @param \WP_Query|\WP_Term_Query $query + * @return void + */ + public function sort($query) + { + return; + } + + /** + * Can the column be sorted. + * + * @return boolean + */ + public function isSortable(): bool + { + return false; + } + + /** + * Check the current column name matches this column. + * + * @param string $name + * @return boolean + */ + public function isColumn(string $name): bool + { + return $name === $this->name(); + } +} diff --git a/src/Columns.php b/src/Columns.php index 14ac23d..cdd7be5 100644 --- a/src/Columns.php +++ b/src/Columns.php @@ -2,239 +2,210 @@ namespace PostTypes; -/** - * Columns - * - * Used to help manage a post types columns in the admin table - * - * @link https://github.com/jjgrainger/PostTypes/ - * @author jjgrainger - * @link https://jjgrainger.co.uk - * @version 2.2.1 - * @license https://opensource.org/licenses/mit-license.html MIT License - */ +use PostTypes\Contracts\ColumnContract; + class Columns { /** - * Holds an array of all the defined columns. - * - * @var array - */ - public $items = []; - - /** - * An array of columns to add. + * Columns to add. * * @var array */ public $add = []; /** - * An array of columns to hide. + * Column populate callbacks. * * @var array */ - public $hide = []; + public $populate = []; /** - * An array of columns to reposition. + * Columns to remove. * * @var array */ - public $positions = []; + public $remove = []; /** - * An array of custom populate callbacks. + * Columns order. * * @var array */ - public $populate = []; + public $order = []; /** - * An array of columns that are sortable. + * Sortable columns and sort callbacks. * * @var array */ public $sortable = []; /** - * Set the all columns - * @param array $columns an array of all the columns to replace + * Add a column object. + * + * @param ColumnContract $column + * @return void */ - public function set($columns) + public function column(ColumnContract $column) { - $this->items = $columns; - } + $this->add($column->name(), $column->label()); - /** - * 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) - { + $this->populate($column->name(), [$column, 'populate']); - if (!is_array($columns)) { - $columns = [$columns => $label]; + if (!is_null($column->order())) { + $this->order[$column->name()] = $column->order(); } - foreach ($columns as $column => $label) { - if (is_null($label)) { - $label = str_replace(['_', '-'], ' ', ucfirst($column)); - } - - $this->add[$column] = $label; + if ($column->isSortable()) { + $this->sortable($column->name(), [$column, 'sort']); } - - return $this; } /** - * Add a column to hide - * @param string $column the slug of the column to hdie + * Add a column. + * + * @param string $key + * @param string $label + * @param callable|null $callback + * @return void */ - public function hide($columns) + public function add(string $key, string $label, callable $callback = null) { - if (!is_array($columns)) { - $columns = [$columns]; - } + $this->add[$key] = $label; - foreach ($columns as $column) { - $this->hide[] = $column; + if (is_callable($callback)) { + $this->populate($key, $callback); } - - return $this; } /** - * Set a custom callback to populate a column - * @param string $column the column slug - * @param mixed $callback callback function + * Set column populate callback. + * + * @param string $key + * @param callable $callback + * @return void */ - public function populate($column, $callback) + public function populate(string $key, callable $callback) { - $this->populate[$column] = $callback; - - return $this; + $this->populate[$key] = $callback; } /** - * Define the postion for a columns - * @param string $columns an array of columns + * Remove columns. + * + * @param array $keys + * @return void */ - public function order($columns) + public function remove(array $keys) { - foreach ($columns as $column => $position) { - $this->positions[$column] = $position; - } - - return $this; + $this->remove = array_merge($this->remove, $keys); } /** - * 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 + * Set columns order + * + * @param array $order + * @return void */ - public function sortable($sortable) + public function order(array $order) { - foreach ($sortable as $column => $options) { - $this->sortable[$column] = $options; - } - - return $this; + $this->order = array_merge($this->order, $order); } /** - * Check if an orderby field is a custom sort option. - * @param string $orderby the orderby value from query params + * Set sortable columns and sort callback. + * + * @param string $key + * @param callable $callback + * @return void */ - public function isSortable($orderby) + public function sortable(string $key, callable $callback) { - 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; + $this->sortable[$key] = $callback; } /** - * Get meta key for an orderby. - * @param string $orderby the orderby value from query params + * Apply columns. + * + * @param array $columns + * @return void */ - public function sortableMeta($orderby) + public function applyColumns(array $columns) { - if (array_key_exists($orderby, $this->sortable)) { - return $this->sortable[$orderby]; + if (!empty($this->add)) { + $columns = array_merge($columns, $this->add); } - 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; + if (!empty($this->remove)) { + $columns = array_diff_key($columns, array_flip($this->remove)); + } + + if (!empty($this->order)) { + $order = $this->order; + + // Sort the order array. + asort($order); + + // Flip order so the index is the position. + $order = array_flip($order); + + // Create the current order array. + $current = array_keys($columns); + + // Loop over the order. + foreach ($order as $index => $key) { + array_splice($current, $index, 0, $key); } + + $new = array_flip(array_unique($current)); + + $columns = array_merge($new, $columns); } - return ''; + return $columns; } /** - * Modify the columns for the object - * @param array $columns WordPress default columns - * @return array The modified columns + * Populate a column. + * + * @param string $column + * @param array $params + * @return void */ - public function modifyColumns($columns) + public function populateColumn(string $column, array $params) { - // if user defined set columns, return those - if (!empty($this->items)) { - return $this->items; + if (isset($this->populate[$column]) && is_callable($this->populate[$column])) { + call_user_func_array($this->populate[$column], $params); } + } - // add additional columns - if (!empty($this->add)) { - foreach ($this->add as $key => $label) { - $columns[$key] = $label; - } + /** + * Set sortable columns + * + * @param array $columns + * @return array + */ + public function setSortable(array $columns): array + { + foreach (array_keys($this->sortable) as $key) { + $columns[$key] = $key; } - // unset hidden columns - if (!empty($this->hide)) { - foreach ($this->hide as $key) { - unset($columns[$key]); - } - } + return $columns; + } - // 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; - } + /** + * Sort a column. + * + * @param string $column + * @param \WP_Query|\WP_Term_Query $query + * @return void + */ + public function sortColumn(string $column, $query) + { + if (isset($this->sortable[$column]) && is_callable($this->sortable[$column])) { + call_user_func_array($this->sortable[$column], [$query]); } - - return $columns; } } diff --git a/src/Contracts/ColumnContract.php b/src/Contracts/ColumnContract.php new file mode 100644 index 0000000..940fc70 --- /dev/null +++ b/src/Contracts/ColumnContract.php @@ -0,0 +1,58 @@ +columns = new Columns; - } + $columns = new Columns; - /** @test */ - public function canCreateColumns() - { - $this->assertInstanceOf(Columns::class, $this->columns); + $columns->add('column', 'Test Column'); + + $this->assertArrayHasKey('column', $columns->add); + $this->assertSame('Test Column', $columns->add['column']); } - /** @test */ - public function canSetColumns() + public function test_can_add_column_with_column_class() { - $columns = [ - 'title' => 'Title', - 'date' => 'Date', - ]; - - $this->columns->set($columns); + $stub = $this->getMockForAbstractClass(Column::class); - $this->assertEquals($this->columns->items, $columns); - } + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('column')); - /** @test */ - public function canAddColumnsWithArray() - { - $columns = [ - 'genre' => 'Genre', - ]; + $columns = new Columns; + $columns->column($stub); - $this->columns->add($columns); + $this->assertArrayHasKey('column', $columns->add); + $this->assertSame('Column', $columns->add['column']); - $this->assertEquals($this->columns->add, $columns); + $this->assertArrayHasKey('column', $columns->populate); + $this->assertIsCallable($columns->populate['column']); } - /** @test */ - public function canAddColumnsWithArgs() + public function test_can_add_column_with_populate_callback() { - $this->columns->add('genre', 'Genre'); + $columns = new Columns; - // Auto generated label1 - $this->columns->add('price'); - - $expected = [ - 'genre' => 'Genre', - 'price' => 'Price', - ]; + $columns->add('column', 'Test Column', function() {}); - $this->assertEquals($this->columns->add, $expected); + $this->assertArrayHasKey('column', $columns->populate); + $this->assertIsCallable($columns->populate['column']); } - /** @test */ - public function canHideColumns() + public function test_can_set_column_populate_callback() { - $columns = [ - 'date' - ]; + $columns = new Columns; - $this->columns->hide($columns); + $columns->populate('column', function() {}); - $this->assertEquals($this->columns->hide, $columns); + $this->assertArrayHasKey('column', $columns->populate); + $this->assertIsCallable($columns->populate['column']); } - /** @test */ - public function canPopulateColumns() + public function test_can_set_remove_column() { - $callable = function($column, $post_id) { - echo $post_id; - }; + $columns = new Columns; - $this->columns->populate('post_id', $callable); + $columns->remove(['column']); - $this->assertEquals($this->columns->populate['post_id'], $callable); + $this->assertEquals(['column'], $columns->remove); } - /** @test */ - public function canOrderColumns() + public function test_can_set_remove_columns_with_multiple_calls() { - $columns = [ - 'date' => 3, - 'genre' => 2 - ]; + $columns = new Columns; - $this->columns->order($columns); + $columns->remove(['column']); + $columns->remove(['column_2']); - $this->assertEquals($this->columns->positions, $columns); + $this->assertEquals(['column', 'column_2'], $columns->remove); } - /** @test */ - public function canSortColumns() + public function test_can_set_order_columns() { - $columns = [ - 'rating' => ['_rating', true] - ]; + $columns = new Columns; - $this->columns->sortable($columns); + $columns->order([ + 'column' => 1, + ]); - $this->assertEquals($this->columns->sortable, $columns); + $this->assertEquals(['column' => 1], $columns->order); } - /** @test */ - public function usesSetColumnsOverDefaults() + public function test_can_set_order_columns_with_multiple_calls() { - $defaults = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; + $columns = new Columns; - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'date' => 'Date' - ]; + $columns->order(['column' => 1]); + $columns->order(['column_2' => 3]); - $this->columns->set($columns); - - $output = $this->columns->modifyColumns($defaults); - - $this->assertEquals($output, $columns); + $this->assertEquals(['column' => 1, 'column_2' => 3], $columns->order); } - /** @test */ - public function addsColumnsToDefaults() + public function test_can_set_sortable_column() { - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; - - $this->columns->add(['genre' => 'Genres']); - - $output = $this->columns->modifyColumns($columns); + $columns = new Columns; - $columns['genre'] = 'Genres'; + $columns->sortable('column', function() {}); - $this->assertEquals($output, $columns); + $this->assertArrayHasKey('column', $columns->sortable); + $this->assertIsCallable($columns->sortable['column']); } - /** @test */ - public function hideColumnsFromDefaults() + public function test_can_apply_columns() { - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; - - $this->columns->hide('comments'); + $columns = new Columns; - $output = $this->columns->modifyColumns($columns); + $columns->add('column_5', 'Column 5'); - unset($columns['comments']); + $columns->remove(['column_2']); - $this->assertEquals($output, $columns); - } + $columns->order([ + 'column_3' => 0, + ]); - /** @test */ - public function setOrderOfDefaultColumns() - { - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' + $original = [ + 'column_1' => 'Column 1', + 'column_2' => 'Column 2', + 'column_3' => 'Column 3', + 'column_4' => 'Column 4', ]; - $this->columns->order([ - 'date' => 1, - 'title' => 3 - ]); - - $output = $this->columns->modifyColumns($columns); + $modified = $columns->applyColumns($original); $expected = [ - 'date' => 'Date', - 'author' => 'Author', - 'title' => 'Title', - 'comments' => 'Comments', + 'column_3' => 'Column 3', + 'column_1' => 'Column 1', + 'column_4' => 'Column 4', + 'column_5' => 'Column 5', ]; - $this->assertEquals($output, $expected); + $this->assertSame($expected, $modified); } - /** @test */ - public function canModifyColumns() + public function test_can_add_sortable_columns_to_sortable_list() { - $defaults = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; - - $expected = [ - 'title' => 'Title', - 'genre' => 'Genre', - 'author' => 'Author', - 'date' => 'Date' - ]; - - $this->columns->hide('comments'); + $columns = new Columns; - $this->columns->add(['genre' => 'Genre']); + $columns->sortable('column', function() {}); - $this->columns->order([ - 'genre' => 2, - ]); - - $output = $this->columns->modifyColumns($defaults); - - $this->assertEquals($output, $expected); - } - - /** @test */ - public function canIdentifySortableColumns() - { - $columns = [ - 'rating' => ['_rating', true], - 'price' => '_price', - 'sortable' => ['sortable'], + $sortable = [ + 'title' => 'title', ]; - $this->columns->sortable($columns); + $sortable = $columns->setSortable($sortable); - $this->assertTrue($this->columns->isSortable('_rating')); - $this->assertTrue($this->columns->isSortable('_price')); - $this->assertTrue($this->columns->isSortable('sortable')); - $this->assertFalse($this->columns->isSortable('not_a_column')); - } - - /** @test */ - public function returnsCorrectSortableMetaKey() - { - $columns = [ - 'rating' => ['_rating', true], - 'price' => '_price', - 'column' => ['sortable'], + $expected = [ + 'title' => 'title', + 'column' => 'column', ]; - $this->columns->sortable($columns); - - $this->assertEquals($this->columns->sortableMeta('rating'), ['_rating', true]); - $this->assertEquals($this->columns->sortableMeta('_price'), '_price'); - $this->assertEquals($this->columns->sortableMeta('sortable'), ['sortable']); - $this->assertEquals($this->columns->sortableMeta('not_a_column'), ''); + $this->assertSame($expected, $sortable); } } From 5b8c8e6412d9fae4677e90179e6a08da5e338efc Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 4 Dec 2024 21:24:23 +0000 Subject: [PATCH 04/41] change Taxonomy to abstract class --- src/Contracts/TaxonomyContract.php | 58 +++++ src/Registrars/TaxonomyRegistrar.php | 172 +++++++------- src/Taxonomy.php | 255 ++++----------------- tests/Registrars/TaxonomyRegistrarTest.php | 11 +- tests/TaxonomyTest.php | 161 +------------ 5 files changed, 186 insertions(+), 471 deletions(-) create mode 100644 src/Contracts/TaxonomyContract.php diff --git a/src/Contracts/TaxonomyContract.php b/src/Contracts/TaxonomyContract.php new file mode 100644 index 0000000..b491d69 --- /dev/null +++ b/src/Contracts/TaxonomyContract.php @@ -0,0 +1,58 @@ +taxonomy = $taxonomy; + + $this->columns = $taxonomy->columns(new Columns()); } + /** + * Register the Taxonomy to WordPress. + * + * @return void + */ public function register() { - // Get the Taxonomy name. - $name = $this->taxonomy->name; - - // Register the taxonomy, set priority to 9 so taxonomies are registered before PostTypes add_action('init', [$this, 'registerTaxonomy'], 9); + add_action('init', [$this, 'registerTaxonomyToPostTypes'], 10); - // Assign taxonomy to post type objects - add_action('init', [$this, 'registerTaxonomyToObjects'], 10); - - if (isset($this->taxonomy->columns)) { - // Modify the columns for the Taxonomy - add_filter("manage_edit-' . $name . '_columns", [$this, 'modifyColumns']); - - // populate the columns for the Taxonomy - add_filter('manage_' . $name . '_custom_column', [$this, 'populateColumns'], 10, 3); - - // set custom sortable columns - add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns']); - - // run action that sorts columns on request - add_action('parse_term_query', [$this, 'sortSortableColumns']); - } + // Handle Taxonomy columns. + add_filter("manage_edit-{$this->taxonomy->name()}_columns", [$this, 'modifyColumns'], 10, 1); + add_filter("manage_{$this->taxonomy->name()}_custom_column", [$this, 'populateColumns'], 10, 3); + add_filter("manage_edit-{$this->taxonomy->name()}_sortable_columns", [$this, 'setSortableColumns'], 10, 1); + add_action('parse_term_query', [$this, 'sortSortableColumns'], 10, 1); } /** - * Register the Taxonomy to WordPress + * Register the Taxonomy. + * * @return void */ public function registerTaxonomy() { - // Get the existing taxonomy options if it exists. - $options = (taxonomy_exists($this->taxonomy->name)) ? (array) get_taxonomy($this->taxonomy->name) : []; - - // create options for the Taxonomy. - $options = array_replace_recursive($options, $this->taxonomy->createOptions()); + register_taxonomy($this->taxonomy->name(), null, $this->generateOptions()); + } - // register the Taxonomy with WordPress. - register_taxonomy($this->taxonomy->name, null, $options); + /** + * Generate Taxonomy options. + * + * @return array + */ + public function generateOptions() + { + $defaults = [ + 'public' => true, + 'show_in_rest' => true, + 'hierarchical' => true, + 'show_admin_column' => true, + 'labels' => $this->taxonomy->labels(), + 'rewrite' => [ + 'slug' => $this->taxonomy->slug(), + ], + ]; + + return array_replace_recursive($defaults, $this->taxonomy->options()); } /** - * Register the Taxonomy to PostTypes + * Register Taxonomy to post types. + * * @return void */ - public function registerTaxonomyToObjects() + public function registerTaxonomyToPostTypes() { - // register Taxonomy to each of the PostTypes assigned - if (empty($this->taxonomy->posttypes)) { - return; - } - - foreach ($this->taxonomy->posttypes as $posttype) { - register_taxonomy_for_object_type($this->taxonomy->name, $posttype); + foreach ($this->taxonomy->posttypes() as $posttype) { + register_taxonomy_for_object_type($this->taxonomy->name(), $posttype); } } /** - * Modify the columns for the Taxonomy - * @param array $columns The WordPress default columns + * Modify the Taxonomy columns. + * + * @param array $columns * @return array */ - public function modifyColumns($columns) + public function modifyColumns(array $columns) { - return $this->taxonomy->columns->modifyColumns($columns); + return $this->columns->applyColumns($columns); } /** - * Populate custom columns for the Taxonomy - * @param string $content - * @param string $column - * @param int $term_id + * Populate Taxonomy column. + * + * @param string $content + * @param string $column + * @param int $term_id + * @return void */ public function populateColumns($content, $column, $term_id) { - if (isset($this->taxonomy->columns->populate[$column])) { - $content = call_user_func_array( - $this->taxonomy->columns()->populate[$column], - [$content, $column, $term_id] - ); - } - - return $content; + $this->columns->populateColumn($column, [$term_id, $content]); } /** - * Make custom columns sortable - * @param array $columns Default WordPress sortable columns + * Set the Taxonomy sortable columns. + * + * @param array $columns + * @return array */ public function setSortableColumns($columns) { - if (!empty($this->taxonomy->columns()->sortable)) { - $columns = array_merge($columns, $this->taxonomy->columns()->sortable); - } - - return $columns; + return $this->columns->setSortable($columns); } /** - * Set query to sort custom columns - * @param WP_Term_Query $query + * Sort Taxonomy column. + * + * @param \WP_Term_Query $query + * @return void */ public function sortSortableColumns($query) { - // don't modify the query if we're not in the post type admin - if (!is_admin() || !in_array($this->taxonomy->name, $query->query_vars['taxonomy'] ?? [])) { + if (!is_admin() || !in_array($this->taxonomy->name(), $query->query_vars['taxonomy'])) { return; } - // check the orderby is a custom ordering - if (isset($_GET['orderby']) && array_key_exists($_GET['orderby'], $this->taxonomy->columns()->sortable)) { - // get the custom sorting options - $meta = $this->taxonomy->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; - } + $column = $query->query_vars['orderby']; + + $this->columns->sortColumn($column, $query); } } diff --git a/src/Taxonomy.php b/src/Taxonomy.php index a0acde5..1fdd318 100644 --- a/src/Taxonomy.php +++ b/src/Taxonomy.php @@ -3,263 +3,86 @@ namespace PostTypes; use PostTypes\Columns; +use PostTypes\Contracts\TaxonomyContract; use PostTypes\Registrars\TaxonomyRegistrar; -/** - * Taxonomy - * - * Create WordPress Taxonomies easily - * - * @link https://github.com/jjgrainger/PostTypes/ - * @author jjgrainger - * @link https://jjgrainger.co.uk - * @version 2.2.1 - * @license https://opensource.org/licenses/mit-license.html MIT License - */ -class Taxonomy +abstract class Taxonomy implements TaxonomyContract { /** - * The names passed to the Taxonomy - * @var mixed + * Taxonomy name. + * + * @return string */ - public $names; + abstract public function name(): string; /** - * The Taxonomy name - * @var string + * Taxonomy slug. + * + * @return string */ - public $name; - - /** - * The singular label for the Taxonomy - * @var string - */ - public $singular; - - /** - * The plural label for the Taxonomy - * @var string - */ - public $plural; - - /** - * The Taxonomy slug - * @var string - */ - public $slug; - - /** - * Custom options for the Taxonomy - * @var array - */ - public $options; - - /** - * Custom labels for the Taxonomy - * @var array - */ - public $labels; - - /** - * PostTypes to register the Taxonomy to - * @var array - */ - public $posttypes = []; - - /** - * The column manager for the Taxonomy - * @var mixed - */ - public $columns; - - /** - * Create a Taxonomy - * @param mixed $names The name(s) for the Taxonomy - */ - public function __construct($names, $options = [], $labels = []) + public function slug(): string { - $this->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; + return $this->name(); } /** - * Set options for the Taxonomy - * @param array $options - * @return $this + * Taxonomy labels. + * + * @return array */ - public function options(array $options = []) + public function labels(): array { - $this->options = $options; - - return $this; + return []; } /** - * Set the Taxonomy labels - * @param array $labels - * @return $this + * Taxonomy options. + * + * @return array */ - public function labels(array $labels = []) + public function options(): array { - $this->labels = $labels; - - return $this; + return []; } /** - * Assign a PostType to register the Taxonomy to - * @param mixed $posttypes - * @return $this + * Taxonomy post types. + * + * @return array */ - public function posttype($posttypes) + public function posttypes(): array { - $posttypes = is_string($posttypes) ? [$posttypes] : $posttypes; - - foreach ($posttypes as $posttype) { - $this->posttypes[] = $posttype; - } - - return $this; + return []; } /** - * Get the Column Manager for the Taxonomy + * Taxonomy columns. + * + * @param Columns $columns * @return Columns */ - public function columns() + public function columns(Columns $columns): Columns { - if (!isset($this->columns)) { - $this->columns = new Columns; - } - - return $this->columns; + return $columns; } /** - * Register the Taxonomy to WordPress + * Taxonomy hooks. + * * @return void */ - public function register() + public function hooks(): void { - (new TaxonomyRegistrar($this))->register(); + return; } /** - * Create names for the Taxonomy + * Register 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() + public function register(): void { - // default labels - $labels = [ - 'name' => $this->plural, - 'singular_name' => $this->singular, - 'menu_name' => $this->plural, - 'all_items' => "All {$this->plural}", - 'edit_item' => "Edit {$this->singular}", - 'view_item' => "View {$this->singular}", - 'update_item' => "Update {$this->singular}", - 'add_new_item' => "Add New {$this->singular}", - 'new_item_name' => "New {$this->singular} Name", - 'parent_item' => "Parent {$this->plural}", - 'parent_item_colon' => "Parent {$this->plural}:", - 'search_items' => "Search {$this->plural}", - 'popular_items' => "Popular {$this->plural}", - 'separate_items_with_commas' => "Seperate {$this->plural} with commas", - 'add_or_remove_items' => "Add or remove {$this->plural}", - 'choose_from_most_used' => "Choose from most used {$this->plural}", - 'not_found' => "No {$this->plural} found", - ]; - - return array_replace($labels, $this->labels); + (new TaxonomyRegistrar($this))->register(); } } diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index 3a2494b..b53455b 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -1,17 +1,8 @@ assertInstanceOf(TaxonomyRegistrar::class, $registrar); - } + // } diff --git a/tests/TaxonomyTest.php b/tests/TaxonomyTest.php index 2883c19..9c7c238 100644 --- a/tests/TaxonomyTest.php +++ b/tests/TaxonomyTest.php @@ -1,167 +1,8 @@ genres = new Taxonomy('genre'); - } - - /** @test */ - public function canCreateTaxonomy() - { - $this->assertInstanceOf(Taxonomy::class, $this->genres); - } - - /** @test */ - public function hasNameOnInstantiation() - { - $this->assertEquals('genre', $this->genres->names['name']); - } - - /** @test */ - public function hasNamesOnInstantiation() - { - $names = [ - 'name' => 'genre', - 'singular' => 'Genre', - 'plural' => 'Genres', - 'slug' => 'genres' - ]; - - $genres = new Taxonomy($names); - - $this->assertEquals($genres->names, $names); - } - - /** @test */ - public function hasOptionsOnInstantiation() - { - $this->assertEquals($this->genres->options, []); - } - - /** @test */ - public function hasCustomOptionsOnInstantiation() - { - $options = [ - 'public' => true, - ]; - - $genres = new Taxonomy('genre', $options); - - $this->assertEquals($genres->options, $options); - } - - /** @test */ - public function hasLabelsOnInstatiation() - { - $this->assertEquals($this->genres->labels, []); - } - - /** @test */ - public function hasCustomLabelsOnInstantiation() - { - $labels = [ - 'name' => 'Genres', - 'add_new' => 'Add New Genre' - ]; - - $genres = new Taxonomy('genre', [], $labels); - - $this->assertEquals($genres->labels, $labels); - } - - /** @test */ - public function posttypesEmptyOnInstantiation() - { - $this->assertEquals($this->genres->posttypes, []); - } - - /** @test */ - public function hasCustomPosttypesWhenAssigned() - { - $genres = new Taxonomy('genre'); - - $genres->posttype('books'); - - $this->assertEquals($genres->posttypes, ['books']); - } - - /** @test */ - public function canAddMultiplePostTypes() - { - $genres = new Taxonomy('genre'); - - $genres->posttype(['books', 'films']); - - $this->assertEquals($genres->posttypes, ['books', 'films']); - } - - /** @test */ - public function namesCreatedFromName() - { - $this->genres->createNames(); - - $this->assertEquals($this->genres->name, 'genre'); - $this->assertEquals($this->genres->singular, 'Genre'); - $this->assertEquals($this->genres->plural, 'Genres'); - $this->assertEquals($this->genres->slug, 'genres'); - } - - /** @test */ - public function passedNamesAreUsed() - { - $names = [ - 'name' => 'genre', - 'singular' => 'Single Genre', - 'plural' => 'Multiple Genres', - 'slug' => 'slug-genres', - ]; - - $this->genres->names($names); - - $this->genres->createNames(); - - $this->assertEquals($this->genres->name, 'genre'); - $this->assertEquals($this->genres->singular, 'Single Genre'); - $this->assertEquals($this->genres->plural, 'Multiple Genres'); - $this->assertEquals($this->genres->slug, 'slug-genres'); - } - - /** @test */ - public function defaultOptionsUsedIfNotSet() - { - // generated options - $options = $this->genres->createOptions(); - - // expected options - $defaults = [ - 'hierarchical' => true, - 'show_admin_column' => true, - 'labels' => $this->genres->createLabels(), - 'rewrite' => [ - 'slug' => $this->genres->slug, - ], - ]; - - $this->assertEquals($options, $defaults); - } - - /** @test */ - public function columnsIsNullOnInstantiation() - { - $this->assertEquals($this->genres->columns, null); - } - - /** @test */ - public function columnsReturnsInstanceOfColumns() - { - $this->assertInstanceOf(Columns::class, $this->genres->columns()); - } + // } From ed869b458ce312208948db464a6d11a674ccdeb2 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Thu, 5 Dec 2024 14:44:20 +0000 Subject: [PATCH 05/41] improve test coverage for Columns --- tests/ColumnsTest.php | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/ColumnsTest.php b/tests/ColumnsTest.php index 12e63df..7500015 100644 --- a/tests/ColumnsTest.php +++ b/tests/ColumnsTest.php @@ -94,6 +94,26 @@ public function test_can_set_order_columns_with_multiple_calls() $this->assertEquals(['column' => 1, 'column_2' => 3], $columns->order); } + public function test_can_order_column_with_column_class() + { + $columns = new Columns; + + $stub = $this->createMock(Column::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('column')); + + $stub->expects($this->any()) + ->method('order') + ->will($this->returnValue(1)); + + + $columns->column($stub); + + $this->assertEquals(['column' => 1], $columns->order); + } + public function test_can_set_sortable_column() { $columns = new Columns; @@ -135,6 +155,25 @@ public function test_can_apply_columns() $this->assertSame($expected, $modified); } + public function test_can_populate_column() + { + $columns = new Columns; + + $stub = $this->createMock(Column::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('column')); + + $stub->expects($this->once()) + ->method('populate') + ->with($this->greaterThan(0)); + + $columns->column($stub); + + $columns->populateColumn('column', [1]); + } + public function test_can_add_sortable_columns_to_sortable_list() { $columns = new Columns; @@ -154,4 +193,27 @@ public function test_can_add_sortable_columns_to_sortable_list() $this->assertSame($expected, $sortable); } + + public function test_can_sort_column() + { + $columns = new Columns; + + $stub = $this->createMock(Column::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('column')); + + $stub->expects($this->once()) + ->method('isSortable') + ->will($this->returnValue(true)); + + $stub->expects($this->once()) + ->method('sort') + ->with($this->greaterThan(0)); + + $columns->column($stub); + + $columns->sortColumn('column', 1); + } } From 52033e80b2c3e63a6c9abef0bc716663d5aee5e7 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Thu, 5 Dec 2024 15:17:36 +0000 Subject: [PATCH 06/41] improve Column test coverage --- src/Column.php | 11 ----------- src/Contracts/ColumnContract.php | 8 -------- tests/ColumnTest.php | 23 +++++++++++++++++++++++ 3 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 tests/ColumnTest.php diff --git a/src/Column.php b/src/Column.php index 6db2b1c..857c7b0 100644 --- a/src/Column.php +++ b/src/Column.php @@ -64,15 +64,4 @@ public function isSortable(): bool { return false; } - - /** - * Check the current column name matches this column. - * - * @param string $name - * @return boolean - */ - public function isColumn(string $name): bool - { - return $name === $this->name(); - } } diff --git a/src/Contracts/ColumnContract.php b/src/Contracts/ColumnContract.php index 940fc70..45c955e 100644 --- a/src/Contracts/ColumnContract.php +++ b/src/Contracts/ColumnContract.php @@ -47,12 +47,4 @@ public function sort($query); * @return boolean */ public function isSortable(): bool; - - /** - * Check the current column name matches this column. - * - * @param string $name - * @return boolean - */ - public function isColumn(string $name): bool; } diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php new file mode 100644 index 0000000..4bafa1a --- /dev/null +++ b/tests/ColumnTest.php @@ -0,0 +1,23 @@ +getMockForAbstractClass(Column::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('price')); + + $this->assertEquals('price', $stub->name()); + $this->assertEquals('Price', $stub->label()); + $this->assertEquals(null, $stub->populate(1)); + $this->assertEquals(null, $stub->order()); + $this->assertEquals(null, $stub->sort(true)); + $this->assertEquals(false, $stub->isSortable()); + } +} From 59262292e6155dd52f93e491d945a6c01d095b70 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Thu, 5 Dec 2024 15:18:10 +0000 Subject: [PATCH 07/41] add PostType tests --- tests/PostTypeTest.php | 19 ++++++- tests/Registrars/PostTypeRegistrarTest.php | 65 +++++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/tests/PostTypeTest.php b/tests/PostTypeTest.php index 891d322..c0bbd4d 100644 --- a/tests/PostTypeTest.php +++ b/tests/PostTypeTest.php @@ -1,8 +1,25 @@ getMockForAbstractClass(PostType::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $this->assertEquals('book', $stub->slug()); + $this->assertEquals([], $stub->labels()); + $this->assertEquals([], $stub->options()); + $this->assertEquals([], $stub->taxonomies()); + $this->assertEquals(['title', 'editor'], $stub->supports()); + $this->assertEquals(null, $stub->icon()); + $this->assertEquals([], $stub->filters()); + $this->assertEquals(null, $stub->hooks()); + } } diff --git a/tests/Registrars/PostTypeRegistrarTest.php b/tests/Registrars/PostTypeRegistrarTest.php index 1ba653b..997cd08 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -1,8 +1,71 @@ getMockForAbstractClass(PostType::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $registrar = new PostTypeRegistrar($stub); + + $this->assertInstanceOf(PostTypeRegistrar::class, $registrar); + } + + public function test_will_modify_post_type() + { + $stub = $this->getMockForAbstractClass(PostType::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $registrar = new PostTypeRegistrar($stub); + + $args = [ + 'public' => false, + ]; + + $options = $registrar->modifyPostType($args, 'book'); + + $expected = [ + 'public' => true, + 'show_in_rest' => true, + 'labels' => [], + 'taxonomies' => [], + 'supports' => ['title', 'editor'], + 'menu_icon' => null, + 'rewrite' => [ + 'slug' => 'book', + ], + ]; + + $this->assertEquals($expected, $options); + } + + public function test_will_not_modify_post_type_if_name_does_not_match() + { + $stub = $this->getMockForAbstractClass(PostType::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $registrar = new PostTypeRegistrar($stub); + + $args = [ + 'public' => false, + ]; + + $options = $registrar->modifyPostType($args, 'post'); + + $this->assertEquals($args, $options); + } } From eb2bda8a68294e8d544a77ba4dff827d0a802d60 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Thu, 5 Dec 2024 15:18:43 +0000 Subject: [PATCH 08/41] add Taxonomy tests --- .gitignore | 1 + src/Registrars/TaxonomyRegistrar.php | 8 +++++--- tests/Registrars/TaxonomyRegistrarTest.php | 16 +++++++++++++++- tests/TaxonomyTest.php | 16 +++++++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7506fef..4b44d2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor .phpunit.result.cache coverage.xml +/coverage diff --git a/src/Registrars/TaxonomyRegistrar.php b/src/Registrars/TaxonomyRegistrar.php index 4c01d33..65c4acd 100644 --- a/src/Registrars/TaxonomyRegistrar.php +++ b/src/Registrars/TaxonomyRegistrar.php @@ -40,13 +40,15 @@ public function __construct(TaxonomyContract $taxonomy) */ public function register() { + $name = $this->taxonomy->name(); + add_action('init', [$this, 'registerTaxonomy'], 9); add_action('init', [$this, 'registerTaxonomyToPostTypes'], 10); // Handle Taxonomy columns. - add_filter("manage_edit-{$this->taxonomy->name()}_columns", [$this, 'modifyColumns'], 10, 1); - add_filter("manage_{$this->taxonomy->name()}_custom_column", [$this, 'populateColumns'], 10, 3); - add_filter("manage_edit-{$this->taxonomy->name()}_sortable_columns", [$this, 'setSortableColumns'], 10, 1); + add_filter('manage_edit-' . $name . '_columns', [$this, 'modifyColumns'], 10, 1); + add_filter('manage_' . $name . '_custom_column', [$this, 'populateColumns'], 10, 3); + add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); add_action('parse_term_query', [$this, 'sortSortableColumns'], 10, 1); } diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index b53455b..811f8cf 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -1,8 +1,22 @@ getMockForAbstractClass(Taxonomy::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $registrar = new TaxonomyRegistrar($stub); + + $this->assertInstanceOf(TaxonomyRegistrar::class, $registrar); + } + } diff --git a/tests/TaxonomyTest.php b/tests/TaxonomyTest.php index 9c7c238..a8049ec 100644 --- a/tests/TaxonomyTest.php +++ b/tests/TaxonomyTest.php @@ -1,8 +1,22 @@ getMockForAbstractClass(Taxonomy::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('genre')); + + $this->assertEquals('genre', $stub->slug()); + $this->assertEquals([], $stub->labels()); + $this->assertEquals([], $stub->options()); + $this->assertEquals([], $stub->posttypes()); + $this->assertEquals(null, $stub->hooks()); + } } From 0464c6f8485955ef44ccbc2db0665bc2e33c4720 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 11 Dec 2024 18:06:52 +0000 Subject: [PATCH 09/41] draft new docs for post types --- docs/post-types/Add-taxonomies.md | 21 --- docs/post-types/Columns.md | 76 --------- docs/post-types/Create-a-post-type.md | 126 +++------------ docs/post-types/Defining-an-icon.md | 26 +++ docs/post-types/Defining-feature-supports.md | 28 ++++ docs/post-types/Defining-filters.md | 28 ++++ docs/post-types/Defining-hooks.md | 38 +++++ docs/post-types/Defining-labels.md | 39 +++++ docs/post-types/Defining-options.md | 29 ++++ docs/post-types/Defining-taxonomies.md | 31 ++++ docs/post-types/Filters.md | 21 --- docs/post-types/Flush-rewrite-rules.md | 9 -- docs/post-types/Menu-icons.md | 10 -- docs/post-types/Modifying-columns.md | 159 +++++++++++++++++++ 14 files changed, 399 insertions(+), 242 deletions(-) delete mode 100644 docs/post-types/Add-taxonomies.md delete mode 100644 docs/post-types/Columns.md create mode 100644 docs/post-types/Defining-an-icon.md create mode 100644 docs/post-types/Defining-feature-supports.md create mode 100644 docs/post-types/Defining-filters.md create mode 100644 docs/post-types/Defining-hooks.md create mode 100644 docs/post-types/Defining-labels.md create mode 100644 docs/post-types/Defining-options.md create mode 100644 docs/post-types/Defining-taxonomies.md delete mode 100644 docs/post-types/Filters.md delete mode 100644 docs/post-types/Flush-rewrite-rules.md delete mode 100644 docs/post-types/Menu-icons.md create mode 100644 docs/post-types/Modifying-columns.md diff --git a/docs/post-types/Add-taxonomies.md b/docs/post-types/Add-taxonomies.md deleted file mode 100644 index 87a060f..0000000 --- a/docs/post-types/Add-taxonomies.md +++ /dev/null @@ -1,21 +0,0 @@ -# Add Taxonomies - -You can add new and existing taxonomies to a post type by passing the taxonomy name to the `taxonomy()` method. - -```php -// Create a book post type. -$books = new PostType( 'book' ); - -// Add the genre taxonomy to the book post type. -$books->taxonomy( 'genre' ); - -// Add the default category taxonomy. -$books->taxonomy( 'category' ); - -// Register the post type to WordPress. -$books->register(); -``` - -This method only attaches the taxonomy to the post type, to _create_ a taxonomy see the [documentation](../taxonomies/Create-a-taxonomy.md) on creating a new taxonomy. - -Taxonomies and post types can be created in any order. diff --git a/docs/post-types/Columns.md b/docs/post-types/Columns.md deleted file mode 100644 index 638dbd1..0000000 --- a/docs/post-types/Columns.md +++ /dev/null @@ -1,76 +0,0 @@ -# Columns - -To modify a post types admin columns use the `column()` manager. It has a variety of methods to help fine tune admin table columns. - -#### Add Columns - -To add columns to the admin edit screen pass an array of column slugs and labels to the `add()` method. - -```php -// Add multiple columns and set their labels. -$books->columns()->add( [ - 'rating' => __( 'Rating' ), - 'price' => __( 'Price' ), -] ); -``` - -#### Hide Columns - -To hide columns pass the column slug to the `hide()` method. For multiple columns pass an array of column slugs. - -```php -$books->columns()->hide( 'author' ); - -$books->columns()->hide( [ 'author', 'date' ] ); -``` - -#### Column Order - -To rearrange columns pass an array of column slugs and position to the `order()` method. Only columns you want to reorder need to be set, not all columns. Positions are based on a zero based index. - -```php -$books->columns()->order( [ - 'rating' => 2, - 'genre' => 4, -] ); -``` - -#### Set Columns - -To set all columns to display pass an array of the column slugs and labels to the `set()` method. This overrides any other configuration set by the methods above. - -```php -$books->columns()->set( [ - 'cb' => '', - 'title' => __( 'Title' ), - 'genre' => __( 'Genres' ), - 'rating' => __( 'Rating' ), - 'date' => __( 'Date' ), -] ); -``` - -#### Populate Columns - -To populate any column use the `populate()` method, by passing the column slug and a callback function. - -```php -$books->columns()->populate( 'rating', function ( $column, $post_id ) { - echo get_post_meta( $post_id, 'rating', true ) . '/10'; -} ); -``` - -#### Sortable Columns - -To define which custom columns are sortable use the `sortable()` method. This method accepts an array of column slugs and an array of sorting options. - -The first option is the `meta_key` to sort the columns by. - -The second option is how the items are ordered, either numerically (`true`) or alphabetically (`false`) by default. - -```php -// Make both the price and rating columns sortable and ordered numerically. -$books->columns()->sortable( [ - 'price' => [ 'price', true ], - 'rating' => [ 'rating', true ], -] ); -``` diff --git a/docs/post-types/Create-a-post-type.md b/docs/post-types/Create-a-post-type.md index 97aa021..86ecea5 100644 --- a/docs/post-types/Create-a-post-type.md +++ b/docs/post-types/Create-a-post-type.md @@ -1,119 +1,35 @@ # Create a Post Type -You can use PostTypes to create a new post type, or work with an [existing post type](#work-with-existing-posttypes). PostTypes can be included in your theme or plugins. - -## Create a new Post Type - -To create a new post type pass the post types name to the class constructor. In order to apply changes to WordPress you must call the `register()` method. +Post types can be made by creating a new class that extends the `PostType` abstract class. All PostType classes require you to implement the `name()` method. Below is an example of a simple Books PostType class to get started. ```php use PostTypes\PostType; -// Create a book post type. -$books = new PostType( 'book' ); - -// Register the post type to WordPress. -$books->register(); -``` - -{% hint style="info" %} -The `register()` method hooks into WordPress and sets up the different actions and filters to create your custom post type. You do not need to add any of your PostTypes code in actions/filters. Doing so may lead to unexpected results. -{% endhint %} - -### Set names - -The post type labels and slugs are automatically generated from the post type name, however, you can set these manually by passing an array of names to the class constructor. - -```php -$names = [ - 'name' => 'book', - 'singular' => 'Book', - 'plural' => 'Books', - 'slug' => 'books', -]; - -$books = new PostType( $names ); - -$books->register(); -``` - -The following names are accepted. - -| Key | Description | Example | -| --- | --- | --- | -| `name` | is the post type name | *required*, singular, lowercase, underscores | -| `singular` | is the singular label for the post type | e.g 'Book', 'Person' | -| `plural` | is the plural label for the post type | e.g 'Books', 'People' | -| `slug` | is the post type slug used in the permalinks | plural, lowercase, hyphens | - -The only required field is the post type's `name`, all others are optional. - -### Set options - -Options for the post type are set by passing an array as the second argument in the class constructor. - -```php -$options = [ - 'has_archive' => false, -]; - -$books = new PostType( 'book', $options ); - -$books->register(); -``` - -Alternatively, you can set options using the `options()` method. - -```php -$books = new PostType( 'book' ); - -$books->options( [ - 'has_archive' => false, -] ); - -$books->register(); -``` - -The options match the arguments passed to the `register_post_type()` WordPress function. All available options are on the [WordPress Codex](https://codex.wordpress.org/Function_Reference/register_post_type#Parameters) - -### Set labels - -You can set the labels for the post type by passing an array as the third argument in the class constructor. - -```php -$labels = [ - 'add_new_item' => __( 'Add new Book' ), -]; - -$books = new PostType( 'book', $options, $labels ); - -$books->register(); +class Books extends PostType +{ + /** + * Returns the post type name to register to WordPress. + * + * @return string + */ + public function name(): string + { + return 'book'; + } +} ``` +## Register PostType to WordPress -Alternatively, you can use the `labels()` method to set the labels for the post type. +Once your PostType class is created the new post type can be registered to WordPress by instantiating the class and calling the `register()` method in your plugin or theme. ```php -$books = new PostType( 'books' ); - -$books->labels( [ - 'add_new_item' => __( 'Add new Book' ), -] ); +// Instantiate the Books PostType class. +$books = new Books; +// Register the books PostType to WordPress. $books->register(); ``` -All available labels are on the [WordPress Codex](https://codex.wordpress.org/Function_Reference/register_post_type#labels) - -## Work with existing Post Types - -To work with existing post types pass the post type name into the constructor. Be careful and avoid using global variables (e.g `$post`) which can lead to unwanted results. - -```php -// Create a PostType object for an existing post type in WordPress. -$blog = new PostType( 'post' ); - -// Make changes to the post type... - -// You still need to register the changes to WordPress. -$blog->register(); -``` +{% hint style="info" %} +The `register()` method hooks into WordPress and sets all the actions and filters required to create your custom post type. You do not need to add any of your PostTypes code in actions/filters. Doing so may lead to unexpected results. +{% endhint %} diff --git a/docs/post-types/Defining-an-icon.md b/docs/post-types/Defining-an-icon.md new file mode 100644 index 0000000..f309206 --- /dev/null +++ b/docs/post-types/Defining-an-icon.md @@ -0,0 +1,26 @@ +# Menu Icons + +WordPress has [Dashicons](https://developer.wordpress.org/resource/dashicons/), an icon font you can use with your custom post types. + +To set the post type icon pass the dashicon icon slug in the `icon()` method. + +```php +use PostTypes\PostType; + +class Books extends PostType +{ + //... + + /** + * Returns the admin menu icon for the Books post type. + * + * @return array + */ + public function icon(): string + { + return 'dashicons-book-alt'; + } +} +``` + +A list of available icons can be found on the [WordPress documentation](https://developer.wordpress.org/resource/dashicons/) diff --git a/docs/post-types/Defining-feature-supports.md b/docs/post-types/Defining-feature-supports.md new file mode 100644 index 0000000..7608660 --- /dev/null +++ b/docs/post-types/Defining-feature-supports.md @@ -0,0 +1,28 @@ +## Define feature supports + +You can define the features your post types supports using the `supports` method. This works similarly to the [`post_type_supports`](https://developer.wordpress.org/reference/functions/post_type_supports/) function in WordPress and returns an array of 'features'. + +The `title` and `editor` features are enabled by default, matching the WordPress defaults. A list of available features can be seen in the [WordPress documentation](https://developer.wordpress.org/reference/functions/post_type_supports/#more-information). + +```php +use PostTypes\PostType; + +class Books extends PostType +{ + //... + + /** + * Returns features the Books post type supports. + * + * @return array + */ + public function supports(): array + { + return [ + 'title', + 'editor', + 'custom-fields', + ]; + } +} +``` diff --git a/docs/post-types/Defining-filters.md b/docs/post-types/Defining-filters.md new file mode 100644 index 0000000..0db583d --- /dev/null +++ b/docs/post-types/Defining-filters.md @@ -0,0 +1,28 @@ +# Defining filters + +Filters that appear for the post type listing admin screen can be defined using the `filters()` method. + +This should return an array of taxonomy slugs that are to be used as dropdown filters for the post type. + +An empty array is returned by default. + +```php +use PostTypes\PostType; + +class Books extends PostType +{ + //... + + /** + * Returns the filters for the Books post type. + * + * @return array + */ + public function filters(): array + { + return [ + 'category', + ]; + } +} +``` diff --git a/docs/post-types/Defining-hooks.md b/docs/post-types/Defining-hooks.md new file mode 100644 index 0000000..7f03b42 --- /dev/null +++ b/docs/post-types/Defining-hooks.md @@ -0,0 +1,38 @@ +# Defining hooks + +Additional hooks are supported with the `hooks()` method. + +Here you can register additional actions and filters to WordPress and allows you to keep logic associated with your post type in one class. + +```php +use PostTypes\PostType; +use WP_Post; + +class Books extends PostType +{ + //... + + /** + * Adds additional hooks for the post type. + * + * @return array + */ + public function hooks(): array + { + add_action( 'save_post_book', [ $this, 'onSave' ], 10, 3 ); + } + + /** + * Run additional logic when saving a Books post. + * + * @param int $post_id + * @param WP_Post $post + * @param bool $update + * @return void + */ + public function onSave(int $post_id, WP_Post $post, bool $update) + { + // Run additional logic when a Books post type is saved... + } +} +``` diff --git a/docs/post-types/Defining-labels.md b/docs/post-types/Defining-labels.md new file mode 100644 index 0000000..548135f --- /dev/null +++ b/docs/post-types/Defining-labels.md @@ -0,0 +1,39 @@ +# Defining labels + +Labels for a PostType are defined in the `labels()` method and should return an array of labels. + +By default, an empty array is returned and the WordPress default labels are used. + +See [`get_post_type_labels()`](https://developer.wordpress.org/reference/functions/get_post_type_labels/) for a full list of supported labels. + +```php +use PostTypes\PostType; + +class Books extends PostType +{ + //... + + /** + * Returns the Books post type labels. + * + * @return array + */ + public function labels(): array + { + return [ + 'name' => __( 'Books', 'my-text-domain' ), + 'singular_name' => __( 'Book', 'my-text-domain' ), + 'menu_name' => __( 'Books', 'my-text-domain' ), + 'all_items' => __( 'Books', 'my-text-domain' ), + 'add_new' => __( 'Add New', 'my-text-domain' ), + 'add_new_item' => __( 'Add New Book', 'my-text-domain' ), + 'edit_item' => __( 'Edit Book', 'my-text-domain' ), + 'new_item' => __( 'New Book', 'my-text-domain' ), + 'view_item' => __( 'View Book', 'my-text-domain' ), + 'search_items' => __( 'Search Books', 'my-text-domain' ), + 'not_found' => __( 'No Books found', 'my-text-domain' ), + 'not_found_in_trash' => __( 'No Books found in Trash', 'my-text-domain' ), + ]; + } +} +``` diff --git a/docs/post-types/Defining-options.md b/docs/post-types/Defining-options.md new file mode 100644 index 0000000..ab9a2ba --- /dev/null +++ b/docs/post-types/Defining-options.md @@ -0,0 +1,29 @@ +# Defining options + +Options for a PostType are defined in the `options()` method and should return an array of valid [WordPress post type options](https://developer.wordpress.org/reference/functions/register_post_type/#parameters). + +By default, an empty array is returned but these options are merged with a generated options array in [PostTypes](#) and whatever options are defined here will overwrite those defaults. + +See [`register_post_type()`](https://developer.wordpress.org/reference/functions/register_post_type/#parameters) for a full list of supported options. + +```php +use PostTypes\PostType; + +class Books extends PostType +{ + //... + + /** + * Returns the options for the Books post type. + * + * @return array + */ + public function options(): array + { + return [ + 'public' => true, + 'show_in_rest' => true, + ]; + } +} +``` diff --git a/docs/post-types/Defining-taxonomies.md b/docs/post-types/Defining-taxonomies.md new file mode 100644 index 0000000..fcf8da2 --- /dev/null +++ b/docs/post-types/Defining-taxonomies.md @@ -0,0 +1,31 @@ +# Defining Taxonomies + +Taxonomies for a PostType can be definied using the `taxonomies()` method. This method should return an array of taxonomy slugs to associate with the post type. + +An empty array is returned by default and no taxonomies are attached to the PostType. + +```php +use PostTypes\PostType; + +class Books extends PostType +{ + //... + + /** + * Returns taxonomies attached to the Books post type. + * + * @return array + */ + public function taxonomies(): array + { + return [ + 'category', + 'genre', + ]; + } +} +``` + +This method only attaches the taxonomy to the post type, to _create_ a taxonomy see the [documentation](../taxonomies/Create-a-taxonomy.md) on creating a new taxonomy. + +Taxonomies and post types can be created and registered in any order. diff --git a/docs/post-types/Filters.md b/docs/post-types/Filters.md deleted file mode 100644 index 9e4d96d..0000000 --- a/docs/post-types/Filters.md +++ /dev/null @@ -1,21 +0,0 @@ -# Filters - -You can set what dropdown filters appear on the post type admin edit screen by passing an array of taxonomy names to the `filters()` method. - -```php -$books->filters( [ 'genre', 'category' ] ); -``` - -The order the filters appear are set by the order of the items in the array. - -```php -// Display the category dropdown first. -$books->filters( [ 'category', 'genre' ] ); -``` - -An empty array will remove all dropdown filters from the admin edit screen. - -```php -// Don't display filters on the admin edit screen. -$books->filters( [] ); -``` diff --git a/docs/post-types/Flush-rewrite-rules.md b/docs/post-types/Flush-rewrite-rules.md deleted file mode 100644 index cfd0786..0000000 --- a/docs/post-types/Flush-rewrite-rules.md +++ /dev/null @@ -1,9 +0,0 @@ - -# Flush Rewrite Rules - -You can programmatically recreate the sites rewrite rules with the `flush()` method. -This is an expensive operation and should be used with caution, see [codex](https://codex.wordpress.org/Function_Reference/flush_rewrite_rules) for more. - -```php -$books->flush(); -``` diff --git a/docs/post-types/Menu-icons.md b/docs/post-types/Menu-icons.md deleted file mode 100644 index 6a8125e..0000000 --- a/docs/post-types/Menu-icons.md +++ /dev/null @@ -1,10 +0,0 @@ -# Menu Icons - -WordPress 3.8 has [Dashicons](https://developer.wordpress.org/resource/dashicons/), an icon font you can use with your custom post types. - -To set the post type icon pass the dashicon icon slug to the `icon()` method. - -```php -$books->icon( 'dashicons-book-alt' ); -``` -A list of available icons can be found on the [WordPress codex](https://developer.wordpress.org/resource/dashicons/) diff --git a/docs/post-types/Modifying-columns.md b/docs/post-types/Modifying-columns.md new file mode 100644 index 0000000..5bb91fa --- /dev/null +++ b/docs/post-types/Modifying-columns.md @@ -0,0 +1,159 @@ +# Modifying columns + +To modify a post types admin columns use the `column()` method. This method accepts the `PostTypes\Columns` manager which has a variety of methods to help fine tune admin table columns. + +#### Adding Columns + +To add columns to the admin edit screen pass an array of column slugs and labels to the `add()` method. + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Add a new price column. + $columns->add( 'price', __( 'Price', 'my-text-domain' ) ); + + // Populate the price column with post meta. + $columns->populate( 'price', function( $post_id ) { + echo '$' . get_post_meta( $post_id, '_price', true ); + } ); + + // Make the price column sortable. + $columns->sortable( 'price', function( WP_Query $query ) { + $query->set( 'orderby', 'meta_value_num' ); + $query->set( 'meta_key', 'price' ); + } ); + + return $columns; + } +} +``` + +#### Populate Columns + +To populate any column use the `populate()` method, by passing the column slug and a callback function. + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + $columns->populate( 'rating', function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ) . '/10'; + } ); + + return $columns; + } +} +``` + +#### Sortable Columns + +To define which custom columns are sortable use the `sort()` method. + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Make the rating column sortable. + $columns->sortable( 'rating', function( WP_Query $query ) { + $query->set( 'orderby', 'meta_value_num' ); + $query->set( 'meta_key', 'rating' ); + } ); + + return $columns; + } +} +``` + +#### Hide Columns + +To hide columns pass the column slug to the `hide()` method. For multiple columns pass an array of column slugs. + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Hide the Author and Date columns + $columns->hide( [ 'author', 'date' ] ); + + return $columns; + } +} +``` + +#### Column Order + +To rearrange columns pass an array of column slugs and position to the `order()` method. Only olumns you want to reorder need to be set, not all columns. + + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Order the new Rating and Genre columns. + $columns->order( [ + 'rating' => 2, + 'genre' => 4, + ] ); + + return $columns; + } +} +``` + + From aa76b94fc50d5cb96e6cdaee193328acdee72e0d Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 11 Dec 2024 18:30:13 +0000 Subject: [PATCH 10/41] update summary --- docs/SUMMARY.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 0ce1fb5..c7ad054 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,14 +1,17 @@ # Table of Contents -* [PostTypes v2.2.1](../README.md) +* [PostTypes v3.0](../README.md) * [Getting Started](Getting-started.md) * [PostTypes](post-types/README.md) * [Create a Post Type](post-types/Create-a-post-type.md) - * [Add Taxonomies](post-types/Add-taxonomies.md) - * [Filters](post-types/Filters.md) - * [Columns](post-types/Columns.md) - * [Menu Icons](post-types/Menu-icons.md) - * [Flush Rewrite Rules](post-types/Flush-rewrite-rules.md) + * [Defining Labels](post-types/Defining-labels.md) + * [Defining Options](post-types/Defining-options.md) + * [Defining taxonomies](post-types/Defining-taxononmies.md) + * [Defining feature supports](post-types/Defining-feature-supports.md) + * [Defining an icon](post-types/Defining-an-icon.md) + * [Defining filters](post-types/Defining-filters.md) + * [Modifying columns](post-types/Modifying-columns.md) + * [Defining hooks](post-types/Defining-hooks.md) * [Taxonomies](taxonomies/README.md) * [Create a Taxonomy](taxonomies/Create-a-taxonomy.md) * [Add to Post Type](taxonomies/Add-to-post-type.md) From e9b25ebbb145829944378f9dc7aae49bb8b66920 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 11 Dec 2024 18:38:54 +0000 Subject: [PATCH 11/41] update post type index --- docs/SUMMARY.md | 2 +- docs/post-types/Modifying-columns.md | 10 +++++----- docs/post-types/README.md | 15 +++++++++------ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c7ad054..9cf4677 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,7 +6,7 @@ * [Create a Post Type](post-types/Create-a-post-type.md) * [Defining Labels](post-types/Defining-labels.md) * [Defining Options](post-types/Defining-options.md) - * [Defining taxonomies](post-types/Defining-taxononmies.md) + * [Defining taxonomies](post-types/Defining-taxonomies.md) * [Defining feature supports](post-types/Defining-feature-supports.md) * [Defining an icon](post-types/Defining-an-icon.md) * [Defining filters](post-types/Defining-filters.md) diff --git a/docs/post-types/Modifying-columns.md b/docs/post-types/Modifying-columns.md index 5bb91fa..5d22e05 100644 --- a/docs/post-types/Modifying-columns.md +++ b/docs/post-types/Modifying-columns.md @@ -2,7 +2,7 @@ To modify a post types admin columns use the `column()` method. This method accepts the `PostTypes\Columns` manager which has a variety of methods to help fine tune admin table columns. -#### Adding Columns +## Adding Columns To add columns to the admin edit screen pass an array of column slugs and labels to the `add()` method. @@ -40,7 +40,7 @@ class Books extends PostType } ``` -#### Populate Columns +## Populate Columns To populate any column use the `populate()` method, by passing the column slug and a callback function. @@ -68,7 +68,7 @@ class Books extends PostType } ``` -#### Sortable Columns +## Sortable Columns To define which custom columns are sortable use the `sort()` method. @@ -98,7 +98,7 @@ class Books extends PostType } ``` -#### Hide Columns +## Hide Columns To hide columns pass the column slug to the `hide()` method. For multiple columns pass an array of column slugs. @@ -125,7 +125,7 @@ class Books extends PostType } ``` -#### Column Order +## Column Order To rearrange columns pass an array of column slugs and position to the `order()` method. Only olumns you want to reorder need to be set, not all columns. diff --git a/docs/post-types/README.md b/docs/post-types/README.md index 380a23c..59cb426 100644 --- a/docs/post-types/README.md +++ b/docs/post-types/README.md @@ -2,9 +2,12 @@ The following section contains information on creating and working with post types. -* [Create a Post Type](Create-a-post-type.md) -* [Add Taxonomies](Add-taxonomies.md) -* [Filters](Filters.md) -* [Columns](Columns.md) -* [Menu Icons](Menu-icons.md) -* [Flush Rewrite Rules](Flush-rewrite-rules.md) +* [Create a Post Type](post-types/Create-a-post-type.md) +* [Defining Labels](post-types/Defining-labels.md) +* [Defining Options](post-types/Defining-options.md) +* [Defining taxonomies](post-types/Defining-taxonomies.md) +* [Defining feature supports](post-types/Defining-feature-supports.md) +* [Defining an icon](post-types/Defining-an-icon.md) +* [Defining filters](post-types/Defining-filters.md) +* [Modifying columns](post-types/Modifying-columns.md) +* [Defining hooks](post-types/Defining-hooks.md) From 6b20452ae76b96d318d8a86c90b69989ea22e729 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 11 Dec 2024 19:16:56 +0000 Subject: [PATCH 12/41] draft new docs for taxonomies --- docs/post-types/Create-a-post-type.md | 1 + docs/post-types/Modifying-columns.md | 2 +- docs/post-types/README.md | 18 +-- docs/taxonomies/Add-to-post-type.md | 16 --- docs/taxonomies/Columns.md | 89 -------------- docs/taxonomies/Create-a-taxonomy.md | 124 ++++++------------- docs/taxonomies/Defining-hooks.md | 44 +++++++ docs/taxonomies/Defining-labels.md | 33 +++++ docs/taxonomies/Defining-options.md | 29 +++++ docs/taxonomies/Defining-post-types.md | 31 +++++ docs/taxonomies/Modifying-columns.md | 160 +++++++++++++++++++++++++ docs/taxonomies/README.md | 7 +- 12 files changed, 350 insertions(+), 204 deletions(-) delete mode 100644 docs/taxonomies/Add-to-post-type.md delete mode 100644 docs/taxonomies/Columns.md create mode 100644 docs/taxonomies/Defining-hooks.md create mode 100644 docs/taxonomies/Defining-labels.md create mode 100644 docs/taxonomies/Defining-options.md create mode 100644 docs/taxonomies/Defining-post-types.md create mode 100644 docs/taxonomies/Modifying-columns.md diff --git a/docs/post-types/Create-a-post-type.md b/docs/post-types/Create-a-post-type.md index 86ecea5..d843efe 100644 --- a/docs/post-types/Create-a-post-type.md +++ b/docs/post-types/Create-a-post-type.md @@ -18,6 +18,7 @@ class Books extends PostType } } ``` + ## Register PostType to WordPress Once your PostType class is created the new post type can be registered to WordPress by instantiating the class and calling the `register()` method in your plugin or theme. diff --git a/docs/post-types/Modifying-columns.md b/docs/post-types/Modifying-columns.md index 5d22e05..362e300 100644 --- a/docs/post-types/Modifying-columns.md +++ b/docs/post-types/Modifying-columns.md @@ -70,7 +70,7 @@ class Books extends PostType ## Sortable Columns -To define which custom columns are sortable use the `sort()` method. +To define which custom columns are sortable use the `sortable()` method. ```php use PostTypes\PostType; diff --git a/docs/post-types/README.md b/docs/post-types/README.md index 59cb426..52f9f3f 100644 --- a/docs/post-types/README.md +++ b/docs/post-types/README.md @@ -2,12 +2,12 @@ The following section contains information on creating and working with post types. -* [Create a Post Type](post-types/Create-a-post-type.md) -* [Defining Labels](post-types/Defining-labels.md) -* [Defining Options](post-types/Defining-options.md) -* [Defining taxonomies](post-types/Defining-taxonomies.md) -* [Defining feature supports](post-types/Defining-feature-supports.md) -* [Defining an icon](post-types/Defining-an-icon.md) -* [Defining filters](post-types/Defining-filters.md) -* [Modifying columns](post-types/Modifying-columns.md) -* [Defining hooks](post-types/Defining-hooks.md) +* [Create a Post Type](Create-a-post-type.md) +* [Defining Labels](Defining-labels.md) +* [Defining Options](Defining-options.md) +* [Defining taxonomies](Defining-taxonomies.md) +* [Defining feature supports](Defining-feature-supports.md) +* [Defining an icon](Defining-an-icon.md) +* [Defining filters](Defining-filters.md) +* [Modifying columns](Modifying-columns.md) +* [Defining hooks](Defining-hooks.md) diff --git a/docs/taxonomies/Add-to-post-type.md b/docs/taxonomies/Add-to-post-type.md deleted file mode 100644 index 95fdd11..0000000 --- a/docs/taxonomies/Add-to-post-type.md +++ /dev/null @@ -1,16 +0,0 @@ -## Add to post type - -You can add a taxonomy to any post type by passing the post type name to the `posttype()` method. - -```php -// Create the genre taxonomy. -$genres = new Taxonomy( 'genre' ); - -// Attach to the book post type. -$genres->posttype( 'books' ); - -// Register changes to WordPress. -$genres->register(); -``` - -Alternatively, you can attach a taxonomy to a post type when creating a post type using its [`taxonomy()`](../post-types/Add-taxonomies.md) method. diff --git a/docs/taxonomies/Columns.md b/docs/taxonomies/Columns.md deleted file mode 100644 index c7bfa45..0000000 --- a/docs/taxonomies/Columns.md +++ /dev/null @@ -1,89 +0,0 @@ -## Columns - -You can now modify `Taxonomy` columns using the same methods as you would for a `PostType`. For example: - -```php -use PostTypes\Taxonomy; - -// Create a taxonomy. -$genres = new Taxonomy( 'genre' ); - -// Add a column to the taxonomy admin table. -$genres->columns()->add( [ - 'popularity' => __( 'Popularity' ), -] ); - -// Register the taxonomy to WordPress. -$genres->register(); -``` - -#### Add Columns - -To add columns to the admin edit screen pass an array of column slugs and labels to the `add()` method. - -```php -// Add columns and set their labels. -$genres->columns()->add( [ - 'popularity' => __( 'Popularity' ), -] ); -``` - -#### Hide Columns - -To hide columns pass the column slug to the `hide()` method. For multiple columns pass an array of column slugs. - -```php -$genres->columns()->hide( 'description' ); -``` - -#### Column Order - -To rearrange columns pass an array of column slugs and position to the `order()` method. Only columns you want to reorder need to be set, not all columns. Positions are based on a zero based index. - -```php -$genres->columns()->order( [ - 'popularity' => 2, -] ); -``` - -#### Set Columns - -To set all columns to display pass an array of the column slugs and labels to the `set()` method. This overrides any other configuration set by the methods above. - -```php -$genres->columns()->set( [ - 'cb' => '', - 'name' => __( 'Name' ), - 'description' => __( 'Description' ), - 'slug' => __( 'Slug' ), - 'popularity' => __( 'Popularity' ), -] ); -``` - -#### Populate Columns - -To populate any column use the `populate()` method, by passing the column slug and a callback function. - -Taxonomy columns work differently to post type columns. The callback receives 3 arguments, the columns content, the column name and the term ID. Also, the [hook used](https://developer.wordpress.org/reference/hooks/manage_this-screen-taxonomy_custom_column/) is a filter, so the column value must be returned. - -```php -$genres->columns()->populate( 'popularity', function ( $content, $column, $term_id ) { - return get_term_meta( $term_id, 'popularity', true ); -} ); -``` - -#### Sortable Columns - -To define which custom columns are sortable use the `sortable()` method. This method accepts an array of column slugs and an array of sorting options. - -The first option is the term `meta_key` to sort the columns by. - -The second option is how the items are ordered, either numerically (`true`) or alphabetically (`false`) by default. - -```php -// Make both the price and rating columns sortable and ordered numerically. -$genres->columns()->sortable( [ - 'popularity' => [ 'popularity', true ], -] ); -``` - diff --git a/docs/taxonomies/Create-a-taxonomy.md b/docs/taxonomies/Create-a-taxonomy.md index b5edfc3..107703a 100644 --- a/docs/taxonomies/Create-a-taxonomy.md +++ b/docs/taxonomies/Create-a-taxonomy.md @@ -9,105 +9,55 @@ To create a new taxonomy pass the taxonomy name to the class constructor. Labels ```php use PostTypes\Taxonomy; -// Create a new taxonomy -$genres = new Taxonomy( 'genre' ); - -// Register the taxonomy to WordPress -$genres->register(); -``` - -#### Set names - -You can define names by passing an array as the first argument. Only `name` is required. - -```php -$names = [ - 'name' => 'genre', - 'singular' => 'Genre', - 'plural' => 'Genres', - 'slug' => 'genres' -]; - -$genres = new Taxonomy( $names ); - -$genres->register(); +class Genres extends Taxonomy +{ + /** + * Returns the taxonomy name to register to WordPress. + * + * @return string + */ + public function name(): string + { + return 'genre'; + } +} ``` -The following names are accepted. - -| Key | Description | Example | -| --- | --- | --- | -| `name` | is the taxonomy name | *required*, singular, lowercase, underscores | -| `singular` | is the singular label for the taxonomy | e.g 'Genre', 'Category' | -| `plural` | is the plural label for the taxonomy | e.g 'Genres', 'Categories' | -| `slug` | is the taxonomy slug used in the permalinks | plural, lowercase, hyphens | - -#### Add options - -You can further customise taxonomies by passing an array of options as the second argument in the constructor. - -```php -$options = [ - 'hierarchical' => false, -]; - -$genres = new Taxonomy( 'genre', $options ); - -$genres->register(); -``` +## Set the slug for the Taxonomy -Alternatively, you can set options using the `options()` method. +By default, the Taxonomy name is used as the slug for the taxonomy too. To change this use the `slug()` method to return a slug string. ```php -$genres = new Taxonomy( 'genre' ); - -$genres->options( [ - 'hierarchical' => false, -] ); +use PostTypes\Taxonomy; -$genres->register(); +class Genres extends Taxonomy +{ + //... + + /** + * Returns the taxonomy slug. + * + * @return string + */ + public function slug(): string + { + return 'genres'; + } +} ``` -The options match the arguments passed to the `register_taxonomy()` WordPress function. All available options are on the [WordPress Codex](https://codex.wordpress.org/Function_Reference/register_taxonomy#Arguments). +## Register the Taxonomy to WordPress -#### Add labels - -You can define the labels for a taxonomy by passing an array as the third argument in the class constructor. +Once your Taxonomy class is created it can be registered to WordPress by instantiating the class and calling the `register()` method in your plugin or theme. ```php -$labels = [ - 'add_new_item' => __( 'Add new Genre' ), -]; - -$genres = new Taxonomy( 'genres', $options, $labels ); +// Instantiate the Genres Taxonomy class. +$genres = new Genres; +// Register the Genres Taxonomy to WordPress. $genres->register(); ``` -Alternatively, you can use the `labels()` method to set the labels for the taxonomy. - -```php -$genres = new Taxonomy( 'genre' ); - -$genres->labels( [ - 'add_new_item' => __( 'Add new Genre' ), -] ); - -$genres->register(); -``` - -All available labels are on the [WordPress Codex](https://codex.wordpress.org/Function_Reference/register_taxonomy) - -## Work with existing Taxonomies - -You can work with existing taxonomies by passing the taxonomy name to the Taxonoy constructor. Once you have made your changes you need to register them to WordPress using the `register()` method. - -```php -// Create a new Taxonomy object for an existing taxonomy. -$tags = new Taxonomy( 'post_tag' ); - -// Modify the taxonomy... - -// Regsiter changes to WordPress. -$tags->register(); -``` +{% hint style="info" %} +The `register()` method hooks into WordPress and sets all the actions and filters required to create your taxonomy. You do not need to add any of your Taxonomy code in actions/filters. Doing so may lead to unexpected results. +{% endhint %} diff --git a/docs/taxonomies/Defining-hooks.md b/docs/taxonomies/Defining-hooks.md new file mode 100644 index 0000000..bdfe5ee --- /dev/null +++ b/docs/taxonomies/Defining-hooks.md @@ -0,0 +1,44 @@ +# Defining hooks + +Additional hooks are supported with the `hooks()` method. + +Here you can register additional actions and filters to WordPress and allows you to keep logic associated with your post type in one class. + +```php +use PostTypes\Taxonomy; + +class Genres extends Taxonomy +{ + //... + + /** + * Adds additional hooks for the post type. + * + * @return array + */ + public function hooks(): array + { + add_action( 'saved_term', [ $this, 'onSave' ], 10, 5 ); + } + + /** + * Run additional logic when saving a term. + * + * @param int $term_id + * @param int $tt_id + * @param string $taxonomy + * @param bool $update + * @param array $args + * @return void + */ + public function onSave(int $term_id, int $tt_id, string $taxonomy, bool $update, array $args) + { + // Check what taxonomy term we are working with... + if ( $taxonomy !== $this->name() ) { + return; + } + + // Run additional logic when a term is saved... + } +} +``` diff --git a/docs/taxonomies/Defining-labels.md b/docs/taxonomies/Defining-labels.md new file mode 100644 index 0000000..e0f3bbc --- /dev/null +++ b/docs/taxonomies/Defining-labels.md @@ -0,0 +1,33 @@ +# Defining labels + +Labels for a Taxonomy are defined in the `labels()` method and should return an array of labels. + +By default, an empty array is returned and the WordPress default labels are used. + +See [`get_taxonomy_labels()`](https://developer.wordpress.org/reference/functions/get_taxonomy_labels/) for a full list of supported labels. + +```php +use PostTypes\Taxonomy; + +class Genres extends Taxonomy +{ + //... + + /** + * Returns the Genres labels. + * + * @return array + */ + public function labels(): array + { + return [ + 'name' => __( 'Genres', 'my-text-domain' ), + 'singular_name' => __( 'Genre', 'my-text-domain' ), + 'search_items' => __( 'Search Genres', 'my-text-domain' ), + 'all_items' => __( 'Genres', 'my-text-domain' ), + 'edit_item' => __( 'Edit Genre', 'my-text-domain' ), + 'view_item' => __( 'View Genre', 'my-text-domain' ), + ]; + } +} +``` diff --git a/docs/taxonomies/Defining-options.md b/docs/taxonomies/Defining-options.md new file mode 100644 index 0000000..f8f6c2e --- /dev/null +++ b/docs/taxonomies/Defining-options.md @@ -0,0 +1,29 @@ +# Defining options + +Options for a Taxonomy are defined in the `options()` method and should return an array of valid [WordPress taxonomy options](https://developer.wordpress.org/reference/functions/register_taxonomy/#parameters). + +By default, an empty array is returned. + +See [`register_taxonomy()`](https://developer.wordpress.org/reference/functions/register_taxonomy/#parameters) for a full list of supported options. + +```php +use PostTypes\Taxonomy; + +class Genres extends Taxonomy +{ + //... + + /** + * Returns the options for the Genres taxonomy. + * + * @return array + */ + public function options(): array + { + return [ + 'public' => true, + 'hierarchical' => true, + ]; + } +} +``` diff --git a/docs/taxonomies/Defining-post-types.md b/docs/taxonomies/Defining-post-types.md new file mode 100644 index 0000000..95eef00 --- /dev/null +++ b/docs/taxonomies/Defining-post-types.md @@ -0,0 +1,31 @@ +# Defining Post Types + +Post types for a Taxonomy can be definied using the `posttypes()` method. This method should return an array of post type names to associate with the taxonomy. + +An empty array is returned by default and no post types are attached to the Taxonomy. + +```php +use PostTypes\Taxonomy; + +class Genres extends Taxonomy +{ + //... + + /** + * Returns post types attached to the Genres taxonomy. + * + * @return array + */ + public function posttypes(): array + { + return [ + 'post', + 'books', + ]; + } +} +``` + +This method only attaches the post type to the taxonomy, to _create_ a post type see the [documentation](../post-types/Create-a-post-type.md) on creating a new post type. + +Taxonomies and post types can be created and registered in any order. diff --git a/docs/taxonomies/Modifying-columns.md b/docs/taxonomies/Modifying-columns.md new file mode 100644 index 0000000..0a5d1a3 --- /dev/null +++ b/docs/taxonomies/Modifying-columns.md @@ -0,0 +1,160 @@ +# Modifying columns + +To modify a taxonomies admin columns use the `column()` method. This method accepts the `PostTypes\Columns` manager which has a variety of methods to help fine tune admin table columns. + +## Adding Columns + +To add columns to the admin list table pass an array of column slugs and labels to the `add()` method. + +```php +use PostTypes\Taxonomy; +use PostTypes\Columns; + +class Genres extends Taxonomy +{ + //... + + /** + * Set the Taxonomy admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Add a new Popularity column. + $columns->add( 'popularity', __( 'Popularity', 'my-text-domain' ) ); + + // Populate the popularity column with term meta. + $columns->populate( 'popularity', function( $term_id ) { + echo '$' . get_term_meta( $term_id, '_popularity', true ); + } ); + + // Make the popularity column sortable. + $columns->sortable( 'popularity', function( WP_Term_Query $query ) { + $query->query_vars['orderby'] = 'meta_value_num'; + $query->query_vars['meta_key'] = 'popularity'; + } ); + + return $columns; + } +} +``` + +## Populate Columns + +To populate any column use the `populate()` method, by passing the column slug and a callback function. + +```php +use PostTypes\Taxonomy; +use PostTypes\Columns; + +class Genres extends Taxonomy +{ + //... + + /** + * Set the Taxonomy admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + $columns->populate( 'popularity', function( $term_id ) { + echo '$' . get_term_meta( $term_id, '_popularity', true ); + } ); + + + return $columns; + } +} +``` + +## Sortable Columns + +To define which custom columns are sortable use the `sortable()` method. + +```php +use PostTypes\Taxonomy; +use PostTypes\Columns; +use WP_Term_Query; + +class Genres extends Taxonomy +{ + //... + + /** + * Set the Taxonomy admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Make the popularity column sortable. + $columns->sortable( 'popularity', function( WP_Term_Query $query ) { + $query->query_vars['orderby'] = 'meta_value_num'; + $query->query_vars['meta_key'] = 'popularity'; + } ); + + return $columns; + } +} +``` + +## Hide Columns + +To hide columns pass the column slug to the `hide()` method. For multiple columns pass an array of column slugs. + +```php +use PostTypes\Taxonomy; +use PostTypes\Columns; + +class Genres extends Taxonomy +{ + //... + + /** + * Set the Taxonomy admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Hide the Description column. + $columns->hide( [ 'description' ] ); + + return $columns; + } +} +``` + +## Column Order + +To rearrange columns pass an array of column slugs and position to the `order()` method. Only olumns you want to reorder need to be set, not all columns. + + +```php +use PostTypes\Taxonomy; +use PostTypes\Columns; + +class Genres extends Taxonomy +{ + //... + + /** + * Set the Taxonomy admin columns. + * + * @return array + */ + public function columns( Columns $column ): Columns + { + // Order the new Popularity column. + $columns->order( [ + 'popularity' => 2, + ] ); + + return $columns; + } +} +``` + + diff --git a/docs/taxonomies/README.md b/docs/taxonomies/README.md index af22a96..042032a 100644 --- a/docs/taxonomies/README.md +++ b/docs/taxonomies/README.md @@ -3,5 +3,8 @@ The following section contains information on creating and working with taxonomies. * [Create a Taxonomy](Create-a-taxonomy.md) -* [Add to Post Type](Add-to-post-type.md) -* [Columns](Columns.md) +* [Defining Labels](Defining-labels.md) +* [Defining Options](Defining-options.md) +* [Defining Post Types](Defining-post-types.md) +* [Modifying Columns](Modifying-columns.md) +* [Defining Hooks](Defining-hooks.md) From 49b1c7b8232b725642c69c349f517fbca4874a80 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 11 Dec 2024 19:18:25 +0000 Subject: [PATCH 13/41] update summary --- docs/SUMMARY.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 9cf4677..12ee8f1 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -14,8 +14,11 @@ * [Defining hooks](post-types/Defining-hooks.md) * [Taxonomies](taxonomies/README.md) * [Create a Taxonomy](taxonomies/Create-a-taxonomy.md) - * [Add to Post Type](taxonomies/Add-to-post-type.md) - * [Columns](taxonomies/Columns.md) + * [Defining Labels](taxonomies/Defining-labels.md) + * [Defining Options](taxonomies/Defining-options.md) + * [Defining Post Types](taxonomies/Defining-post-types.md) + * [Modifying Columns](taxonomies/Modifying-columns.md) + * [Defining Hooks](taxonomies/Defining-hooks.md) * [Notes](Notes.md) * [Contributing](../CONTRIBUTING.md) * [Changelog](../Changelog.md) From 00d8241b277ffae931bbc8171dec58a5afbe9988 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 19 Feb 2025 14:30:48 +0000 Subject: [PATCH 14/41] improve Registrar tests --- tests/Registrars/PostTypeRegistrarTest.php | 99 +++++++++++++++ tests/Registrars/TaxonomyRegistrarTest.php | 137 ++++++++++++++++++++- 2 files changed, 235 insertions(+), 1 deletion(-) diff --git a/tests/Registrars/PostTypeRegistrarTest.php b/tests/Registrars/PostTypeRegistrarTest.php index 997cd08..848ef71 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -1,6 +1,8 @@ assertEquals($args, $options); } + + public function test_can_modify_columns() + { + $defaults = [ + 'cb' => '', + 'title' => 'Title', + 'author' => 'Author', + ]; + + $columns = new Columns; + $columns->add('date', 'Date', function() {}); + + $stub = $this->getMockBuilder(PostType::class) + ->getMock(); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $stub->expects($this->once()) + ->method('columns') + ->will($this->returnValue($columns)); + + $registrar = new PostTypeRegistrar($stub); + $output = $registrar->modifyColumns($defaults); + + $expected = [ + 'cb' => '', + 'title' => 'Title', + 'author' => 'Author', + 'date' => 'Date', + ]; + + $this->assertEquals($expected, $output); + } + + public function test_can_populate_column() + { + $columns = new Columns; + + $stub = $this->createMock(Column::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('column')); + + $stub->expects($this->once()) + ->method('populate') + ->will($this->returnValue(true)); + + $columns->column($stub); + + $stub = $this->getMockBuilder(PostType::class) + ->getMock(); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $stub->expects($this->once()) + ->method('columns') + ->will($this->returnValue($columns)); + + $registrar = new PostTypeRegistrar($stub); + $registrar->populateColumns('column', 1); + } + + public function test_can_set_sortable_columns() + { + $columns = new Columns; + $columns->sortable('column', function() {}); + + $sortable = [ + 'title' => 'title', + ]; + + $stub = $this->getMockBuilder(PostType::class) + ->getMock(); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('book')); + + $stub->expects($this->once()) + ->method('columns') + ->will($this->returnValue($columns)); + + $registrar = new PostTypeRegistrar($stub); + $output = $registrar->setSortableColumns($sortable); + + $expected = [ + 'title' => 'title', + 'column' => 'column', + ]; + + $this->assertEquals($expected, $output); + } } diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index 811f8cf..95b007f 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -1,6 +1,8 @@ expects($this->any()) ->method('name') - ->will($this->returnValue('book')); + ->will($this->returnValue('genre')); $registrar = new TaxonomyRegistrar($stub); $this->assertInstanceOf(TaxonomyRegistrar::class, $registrar); } + public function test_can_generate_options_with_overrides() + { + $stub = $this->getMockBuilder(Taxonomy::class) + ->getMock(); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('genre')); + + $stub->expects($this->any()) + ->method('slug') + ->will($this->returnValue('genre')); + + + $stub->expects($this->once()) + ->method('options') + ->will($this->returnValue([ + 'public' => false, + ])); + + + $registrar = new TaxonomyRegistrar($stub); + + $options = $registrar->generateOptions(); + + $expected = [ + 'public' => false, + 'show_in_rest' => true, + 'hierarchical' => true, + 'show_admin_column' => true, + 'labels' => [], + 'rewrite' => [ + 'slug' => 'genre', + ], + ]; + + $this->assertEquals($expected, $options); + } + + public function test_can_modify_columns() + { + $defaults = [ + 'cb' => '', + 'name' => 'Name', + ]; + + $columns = new Columns; + $columns->add('popularity', 'Popularity', function() {}); + + $stub = $this->getMockBuilder(Taxonomy::class) + ->getMock(); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('genre')); + + $stub->expects($this->once()) + ->method('columns') + ->will($this->returnValue($columns)); + + $registrar = new TaxonomyRegistrar($stub); + $output = $registrar->modifyColumns($defaults); + + $expected = [ + 'cb' => '', + 'name' => 'Name', + 'popularity' => 'Popularity', + ]; + + $this->assertEquals($expected, $output); + } + + public function test_can_populate_column() + { + $columns = new Columns; + + $stub = $this->createMock(Column::class); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('column')); + + $stub->expects($this->once()) + ->method('populate') + ->will($this->returnValue(true)); + + $columns->column($stub); + + $stub = $this->getMockBuilder(Taxonomy::class) + ->getMock(); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('genre')); + + $stub->expects($this->once()) + ->method('columns') + ->will($this->returnValue($columns)); + + $registrar = new TaxonomyRegistrar($stub); + $registrar->populateColumns('', 'column', 1); + } + + public function test_can_set_sortable_columns() + { + $columns = new Columns; + $columns->sortable('column', function() {}); + + $sortable = [ + 'title' => 'title', + ]; + + $stub = $this->getMockBuilder(Taxonomy::class) + ->getMock(); + + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('genre')); + + $stub->expects($this->once()) + ->method('columns') + ->will($this->returnValue($columns)); + + $registrar = new TaxonomyRegistrar($stub); + $output = $registrar->setSortableColumns($sortable); + + $expected = [ + 'title' => 'title', + 'column' => 'column', + ]; + + $this->assertEquals($expected, $output); + } } From 8d77c389f792899af7172eae15e33a56d8faf79b Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 16 Jul 2025 14:12:29 +0100 Subject: [PATCH 15/41] update README --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 782016a..c198cce 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,24 @@ require __DIR__ . '/vendor/autoload.php'; use PostTypes\PostType; -$books = new PostType( 'book' ); - +// Create a Books PostType. +class Books extends PostType +{ + /** + * Returns the post type name to register to WordPress. + * + * @return string + */ + public function name(): string + { + return 'book'; + } +} + +// Instantiate the Books PostType class. +$books = new Books; + +// Register the books PostType to WordPress. $books->register(); ``` From 3af053f251830ef29242c596c2ca53d33a3e1572 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 22 Jul 2025 18:22:32 +0100 Subject: [PATCH 16/41] create columns on init to fix issue with translations --- src/Registrars/PostTypeRegistrar.php | 39 +++++++++++++--------- src/Registrars/TaxonomyRegistrar.php | 16 +++++++-- tests/Registrars/PostTypeRegistrarTest.php | 3 ++ tests/Registrars/TaxonomyRegistrarTest.php | 3 ++ 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/Registrars/PostTypeRegistrar.php b/src/Registrars/PostTypeRegistrar.php index 73db695..6923693 100644 --- a/src/Registrars/PostTypeRegistrar.php +++ b/src/Registrars/PostTypeRegistrar.php @@ -29,8 +29,6 @@ class PostTypeRegistrar public function __construct(PostTypeContract $posttype) { $this->posttype = $posttype; - - $this->columns = $posttype->columns(new Columns()); } /** @@ -42,13 +40,9 @@ public function register() { $name = $this->posttype->name(); - if (post_type_exists($name)) { - // Modify the existing PostType if it exists. - add_filter('register_post_type_args', [$this, 'modifyPostType'], 10, 2); - } else { - // Register the new PostType to WordPress. - add_action('init', [$this, 'registerPostType'], 10, 0); - } + // Initialize the post type. + add_action('init', [$this, 'createColumns'], 10, 0); + add_action('init', [$this, 'initialize'], 10, 0); // Handle PostType filters. add_action('restrict_manage_posts', [$this, 'modifyFilters'], 10, 2); @@ -64,12 +58,30 @@ public function register() } /** - * Register the PostType. + * Create Columns. + * + * @return void + */ + public function createColumns() + { + $this->columns = $this->posttype->columns(new Columns()); + } + + /** + * Register Post Type. * * @return void */ - public function registerPostType() + public function initialize() { + // Modify the existing PostType if it exists. + if (post_type_exists($this->posttype->name())) { + add_filter('register_post_type_args', [$this, 'modifyPostType'], 10, 2); + + return; + } + + // Register the new PostType to WordPress. register_post_type($this->posttype->name(), $this->generateOptions()); } @@ -86,10 +98,7 @@ public function modifyPostType(array $args, string $posttype) return $args; } - // create options for the PostType. - $options = $this->generateOptions(); - - return array_replace_recursive($args, $options); + return array_replace_recursive($args, $this->generateOptions()); } /** diff --git a/src/Registrars/TaxonomyRegistrar.php b/src/Registrars/TaxonomyRegistrar.php index 65c4acd..1548c06 100644 --- a/src/Registrars/TaxonomyRegistrar.php +++ b/src/Registrars/TaxonomyRegistrar.php @@ -29,8 +29,6 @@ class TaxonomyRegistrar public function __construct(TaxonomyContract $taxonomy) { $this->taxonomy = $taxonomy; - - $this->columns = $taxonomy->columns(new Columns()); } /** @@ -44,12 +42,26 @@ public function register() add_action('init', [$this, 'registerTaxonomy'], 9); add_action('init', [$this, 'registerTaxonomyToPostTypes'], 10); + add_action('init', [$this, 'createcolumns'], 10); // Handle Taxonomy columns. add_filter('manage_edit-' . $name . '_columns', [$this, 'modifyColumns'], 10, 1); add_filter('manage_' . $name . '_custom_column', [$this, 'populateColumns'], 10, 3); add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); add_action('parse_term_query', [$this, 'sortSortableColumns'], 10, 1); + + // Register custom hooks. + $this->taxonomy->hooks(); + } + + /** + * Create Columns. + * + * @return void + */ + public function createColumns() + { + $this->columns = $this->taxonomy->columns(new Columns()); } /** diff --git a/tests/Registrars/PostTypeRegistrarTest.php b/tests/Registrars/PostTypeRegistrarTest.php index 848ef71..eb480e2 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -94,6 +94,7 @@ public function test_can_modify_columns() ->will($this->returnValue($columns)); $registrar = new PostTypeRegistrar($stub); + $registrar->createColumns(); $output = $registrar->modifyColumns($defaults); $expected = [ @@ -134,6 +135,7 @@ public function test_can_populate_column() ->will($this->returnValue($columns)); $registrar = new PostTypeRegistrar($stub); + $registrar->createColumns(); $registrar->populateColumns('column', 1); } @@ -158,6 +160,7 @@ public function test_can_set_sortable_columns() ->will($this->returnValue($columns)); $registrar = new PostTypeRegistrar($stub); + $registrar->createColumns(); $output = $registrar->setSortableColumns($sortable); $expected = [ diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index 95b007f..15e62a9 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -82,6 +82,7 @@ public function test_can_modify_columns() ->will($this->returnValue($columns)); $registrar = new TaxonomyRegistrar($stub); + $registrar->createColumns(); $output = $registrar->modifyColumns($defaults); $expected = [ @@ -121,6 +122,7 @@ public function test_can_populate_column() ->will($this->returnValue($columns)); $registrar = new TaxonomyRegistrar($stub); + $registrar->createColumns(); $registrar->populateColumns('', 'column', 1); } @@ -145,6 +147,7 @@ public function test_can_set_sortable_columns() ->will($this->returnValue($columns)); $registrar = new TaxonomyRegistrar($stub); + $registrar->createColumns(); $output = $registrar->setSortableColumns($sortable); $expected = [ From 09778c173c8376e68cd58942dd8731976511119f Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Thu, 24 Jul 2025 19:02:49 +0100 Subject: [PATCH 17/41] update README and getting started --- README.md | 141 +++++++++++++++++++++++++-------------- docs/Getting-started.md | 143 ++++++++++++++++++++++++++++------------ 2 files changed, 194 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index c198cce..b9443ba 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ ## Requirements -* PHP >=7.2 +* PHP >=8.1 * [Composer](https://getcomposer.org/) -* [WordPress](https://wordpress.org) >=5.1 +* [WordPress](https://wordpress.org) >=6.3 ## Installation @@ -20,80 +20,123 @@ Run the following in your terminal to install PostTypes with [Composer](https:// $ composer require jjgrainger/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. +PostTypes uses [PSR-4](https://www.php-fig.org/psr/psr-4/) autoloading and can be used with the Composer's autoloader. 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 + +#### Create a custom post type + +Custom post types are defined as classes that extend the base `PostType` class. At a minimum, the `name` method must be implemented to define the post type slug. All other methods are optional and allow you to configure labels, options, taxonomies, admin columns, filters, and more as needed. ```php -require __DIR__ . '/vendor/autoload.php'; +register(); -``` - -See Composer's [basic usage](https://getcomposer.org/doc/01-basic-usage.md#autoloading) guide for details on working with Composer and autoloading. + /** + * Define the Post Type labels. + */ + public function labels(): array { + return [ + 'name' => __( 'Book', 'text-domain' ), + 'singular_name' => __( 'Book', 'text-domain' ), + 'menu_name' => __( 'Books', 'text-domain' ), + 'all_items' => __( 'Books', 'text-domain' ), + 'add_new' => __( 'Add New', 'text-domain' ), + 'add_new_item' => __( 'Add New Book', 'text-domain' ), + 'edit_item' => __( 'Edit Book', 'text-domain' ), + 'new_item' => __( 'New Book', 'text-domain' ), + 'view_item' => __( 'View Book', 'text-domain' ), + 'search_items' => __( 'Search Books', 'text-domain' ), + 'not_found' => __( 'No Books found', 'text-domain' ), + 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain'), + 'parent_item_colon' => __( 'Parent Book', 'text-domain' ), + ]; + } -## Basic Usage + /** + * Define Post Type feature supports. + */ + public function supports(): array { + return [ + 'title', + 'editor', + 'thumbnail', + 'custom-fields' + ]; + } -Below is a basic example of setting up a simple book post type with a genre taxonomy. For more information, check out the [online documentation here](https://posttypes.jjgrainger.co.uk). + /** + * Define Taxonomies associated with the Post Type. + */ + public function taxonomies(): array { + return [ + 'genre' + 'category', + ]; + } -```php -// Require the Composer autoloader. -require __DIR__ . '/vendor/autoload.php'; + /** + * Set the menu icon for the Post Type. + */ + public function icon(): string { + return 'dashicons-book'; + } -// Import PostTypes. -use PostTypes\PostType; -use PostTypes\Taxonomy; + /** + * Set the admin post table filters. + */ + public function filters(): array { + return [ + 'genre', + 'category' + ]; + } -// Create a book post type. -$books = new PostType( 'book' ); + /** + * Define the columns for the admin post table. + */ + public function columns(Columns $columns): Columns { + // Remove the author and date column. + $columns->remove( [ 'author', 'date' ] ); -// Attach the genre taxonomy (which is created below). -$books->taxonomy( 'genre' ); + // Add a Rating column. + $columns->add( 'rating', __( 'Rating', 'post-types' ) ); -// Hide the date and author columns. -$books->columns()->hide( [ 'date', 'author' ] ); + // Populate the rating column. + $columns->populate( 'rating', function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ); -// Set the Books menu icon. -$books->icon( 'dashicons-book-alt' ); + return $columns; + } +} +``` -// Register the post type to WordPress. -$books->register(); +### Register a custom post type -// Create a genre taxonomy. -$genres = new Taxonomy( 'genre' ); +Once the custom post type class is created it can be registered to WordPress by instantiating and call the register method. -// Set options for the taxonomy. -$genres->options( [ - 'hierarchical' => false, -] ); +```php +// Instantiate the Books PostType class. +$books = new Books; -// Register the taxonomy to WordPress. -$genres->register(); +// Register the books PostType to WordPress. +$books->register(); ``` ## Notes * The full documentation can be found online at [posttypes.jjgrainger.co.uk](https://posttypes.jjgrainger.co.uk) -* The class has no methods for making custom fields for post types, use [Advanced Custom Fields](https://advancedcustomfields.com) -* The book's example used in the README.md can be found in [examples/books.php](examples/books.php) * Licensed under the [MIT License](https://github.com/jjgrainger/PostTypes/blob/master/LICENSE) * Maintained under the [Semantic Versioning Guide](https://semver.org) diff --git a/docs/Getting-started.md b/docs/Getting-started.md index 628a53e..532b2f8 100644 --- a/docs/Getting-started.md +++ b/docs/Getting-started.md @@ -2,9 +2,9 @@ ## Requirements -* PHP >=7.2 +* PHP >=8.1 * [Composer](https://getcomposer.org/) -* [WordPress](https://wordpress.org) >=5.1 +* [WordPress](https://wordpress.org) >=6.3 ## Installation @@ -16,55 +16,116 @@ Run the following in your terminal to install PostTypes with [Composer](https:// $ composer require jjgrainger/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. +PostTypes uses [PSR-4](https://www.php-fig.org/psr/psr-4/) autoloading and can be used with the Composer's autoloader. 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. +#### Create a custom post type + +Custom post types are defined as classes that extend the base `PostType` class. At a minimum, the `name` method must be implemented to define the post type slug. All other methods are optional and allow you to configure labels, options, taxonomies, admin columns, filters, and more as needed. ```php -// Require the Composer autoloader. -require __DIR__ . '/vendor/autoload.php'; + __( 'Book', 'text-domain' ), + 'singular_name' => __( 'Book', 'text-domain' ), + 'menu_name' => __( 'Books', 'text-domain' ), + 'all_items' => __( 'Books', 'text-domain' ), + 'add_new' => __( 'Add New', 'text-domain' ), + 'add_new_item' => __( 'Add New Book', 'text-domain' ), + 'edit_item' => __( 'Edit Book', 'text-domain' ), + 'new_item' => __( 'New Book', 'text-domain' ), + 'view_item' => __( 'View Book', 'text-domain' ), + 'search_items' => __( 'Search Books', 'text-domain' ), + 'not_found' => __( 'No Books found', 'text-domain' ), + 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain'), + 'parent_item_colon' => __( 'Parent Book', 'text-domain' ), + ]; + } + + /** + * Define Post Type feature supports. + */ + public function supports(): array { + return [ + 'title', + 'editor', + 'thumbnail', + 'custom-fields' + ]; + } + + /** + * Define Taxonomies associated with the Post Type. + */ + public function taxonomies(): array { + return [ + 'genre' + 'category', + ]; + } + + /** + * Set the menu icon for the Post Type. + */ + public function icon(): string { + return 'dashicons-book'; + } + + /** + * Set the admin post table filters. + */ + public function filters(): array { + return [ + 'genre', + 'category' + ]; + } + + /** + * Define the columns for the admin post table. + */ + public function columns(Columns $columns): Columns { + // Remove the author and date column. + $columns->remove( [ 'author', 'date' ] ); + + // Add a Rating column. + $columns->add( 'rating', __( 'Rating', 'post-types' ) ); + + // Populate the rating column. + $columns->populate( 'rating', function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ); + + return $columns; + } +} +``` -// Attach the genre taxonomy (which is created below). -$books->taxonomy( 'genre' ); +### Register a custom post type -// Hide the date and author columns. -$books->columns()->hide( [ 'date', 'author' ] ); +Once the custom post type class is created it can be registered to WordPress by instantiating and call the register method. -// Set the Books menu icon. -$books->icon( 'dashicons-book-alt' ); +```php +// Instantiate the Books PostType class. +$books = new Books; -// Register the post type to WordPress. +// Register the books PostType 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(); ``` From f2928a69ed2dec6f2b291e72874b1512af3f6bd2 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Thu, 24 Jul 2025 19:03:16 +0100 Subject: [PATCH 18/41] remove notes --- docs/Notes.md | 28 ---------------------------- docs/SUMMARY.md | 1 - 2 files changed, 29 deletions(-) delete mode 100644 docs/Notes.md diff --git a/docs/Notes.md b/docs/Notes.md deleted file mode 100644 index 0eac9fb..0000000 --- a/docs/Notes.md +++ /dev/null @@ -1,28 +0,0 @@ -# Notes - -## Translations - -Since 2.0 the `translation()` method has been removed. You can translate any labels and names when you assign them to the PostType or Taxonomy. It was removed to provide more control to the developer while encouraging best practices around internationalizing plugins and themes set out by [WordPress](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/). - -```php -// Translating the post types plural and singular names. -$books = new PostType( [ - 'name' => 'book', - 'singular' => __( 'Book', 'YOUR_TEXTDOMAIN' ), - 'plural' => __( 'Books', 'YOUR_TEXTDOMAIN' ), - 'slug' => 'books', -] ); - -// Translating labels. -$books->labels( [ - 'add_new_item' => __( 'Add new Book', 'YOUR_TEXTDOMAIN' ), -] ); -``` - -## Custom Fields - -The class has no methods for making custom fields for post types, use [Advanced Custom Fields](https://advancedcustomfields.com). - -## Examples - -The books example used in the README.md can be found in [examples/books.php](https://github.com/jjgrainger/posttypes/blob/master/examples/books.php). diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 12ee8f1..0fcd9c0 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -19,6 +19,5 @@ * [Defining Post Types](taxonomies/Defining-post-types.md) * [Modifying Columns](taxonomies/Modifying-columns.md) * [Defining Hooks](taxonomies/Defining-hooks.md) -* [Notes](Notes.md) * [Contributing](../CONTRIBUTING.md) * [Changelog](../Changelog.md) From d9c2ae47e45798e5ddd2256c6e537c501de4eedd Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Fri, 1 Aug 2025 08:33:21 +0100 Subject: [PATCH 19/41] add documentation for creating columns --- docs/SUMMARY.md | 1 + docs/post-types/Creating-columns.md | 70 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 docs/post-types/Creating-columns.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 0fcd9c0..ae46bff 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -11,6 +11,7 @@ * [Defining an icon](post-types/Defining-an-icon.md) * [Defining filters](post-types/Defining-filters.md) * [Modifying columns](post-types/Modifying-columns.md) + * [Creating columns](post-types/Creating-columns.md) * [Defining hooks](post-types/Defining-hooks.md) * [Taxonomies](taxonomies/README.md) * [Create a Taxonomy](taxonomies/Create-a-taxonomy.md) diff --git a/docs/post-types/Creating-columns.md b/docs/post-types/Creating-columns.md new file mode 100644 index 0000000..f1fc00e --- /dev/null +++ b/docs/post-types/Creating-columns.md @@ -0,0 +1,70 @@ +# Custom Columns + +The `Column` class allows developers to create reusable, self-contained columns for the post listing table in the WordPress admin. These custom columns can display post meta, taxonomy values, or any custom data related to the post. + +Columns are defined by extending the abstract `PostTypes\Column` class and implementing the required `name()` method, along with any optional logic such as rendering, sorting, or changing the label. + +## Creating a Custom Column + +To create a custom column, extend the base `Column` class and implement the methods you need. Here's an example of a `PriceColumn` that pulls a `_price` meta field from the post and displays it in the admin list table: + +```php +namespace App\Columns; + +use PostTypes\Column; + +class PriceColumn extends Column +{ + public function name(): string + { + return 'price'; + } + + public function label(): string + { + return __( 'Price', 'my-text-domain' ); + } + + public function populate( int $post_id ): void + { + echo '$' . get_post_meta( $post_id, '_price', true ); + } + + public function isSortable(): bool + { + return true; + } + + public function sort(\WP_Query $query): void + { + $query->set( 'meta_key', '_price' ); + $query->set( 'orderby', 'meta_value_num' ); + } +} +``` + +- `name()` defines the column key used internally. +- `label()` sets the visible column heading. +- `populate()` is called when rendering the column for each row. +- `isSortable()` and `sort()` handle sorting logic. + +## Adding the Column to a Post Type + +Once you’ve defined your custom column, you can add it to a PostType using the `$columns->add()` method inside your `PostType` class: + +```php +use App\Columns\PriceColumn; +use PostTypes\PostType; + +class Book extends PostType +{ + //... + + public function columns($columns): void + { + $columns->add(new PriceColumn); + + return $columns; + } +} +``` From 7c56dbefdb0ce63963ff4a75603d0340f218935d Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Fri, 1 Aug 2025 08:33:31 +0100 Subject: [PATCH 20/41] update examples --- examples/Genres.php | 66 ++++++++++++++++++++++++ examples/Price.php | 31 ++++++++++++ examples/books.php | 120 ++++++++++++++++++++++++++++---------------- 3 files changed, 174 insertions(+), 43 deletions(-) create mode 100644 examples/Genres.php create mode 100644 examples/Price.php diff --git a/examples/Genres.php b/examples/Genres.php new file mode 100644 index 0000000..5086a7a --- /dev/null +++ b/examples/Genres.php @@ -0,0 +1,66 @@ +remove(['posts']); + + $columns->add( + 'popularity', + __( 'Popularity', 'post-types' ), + function( $term_id ) { + echo get_term_meta( $term_id, 'popularity', true ); + } + ); + + $columns->order( [ + 'popularity' => 2, + ] ); + + $columns->sortable( 'popularity', function( $query ) { + $query->query_vars['orderby'] = 'meta_value'; + $query->query_vars['meta_key'] = 'popularity'; + } ); + + return $columns; + } + + public function labels(): array { + return [ + 'name' => __( 'Genres', 'post-types' ), + 'singular_name' => __( 'Genre', 'post-types' ), + 'menu_name' => __( 'Genres', 'post-types' ), + 'all_items' => __( 'All Genres', 'post-types' ), + 'edit_item' => __( 'Edit Genre', 'post-types' ), + 'view_item' => __( 'View Genre', 'post-types' ), + 'update_item' => __( 'Update Genre', 'post-types' ), + 'add_new_item' => __( 'Add New Genre', 'post-types' ), + 'new_item_name' => __( 'New Genre', 'post-types' ), + 'parent_item' => __( 'Parent Genres', 'post-types' ), + 'parent_item_colon' => __( 'Parent Genres: ', 'post-types' ), + 'search_items' => __( 'Search Genres', 'post-types' ), + 'popular_items' => __( 'Popular Genres', 'post-types' ), + 'separate_items_with_commas' => __( 'Seperate Genres with commas', 'post-types' ), + 'add_or_remove_items' => __( 'Add or remove Genres', 'post-types' ), + 'choose_from_most_used' => __( 'Choose from most used Genres', 'post-types' ), + 'not_found' => __( 'No Genres found', 'post-types' ), + ]; + } +} diff --git a/examples/Price.php b/examples/Price.php new file mode 100644 index 0000000..6246a79 --- /dev/null +++ b/examples/Price.php @@ -0,0 +1,31 @@ +set('orderby', 'meta_value_num'); + $query->set('meta_key', 'price'); + } +} diff --git a/examples/books.php b/examples/books.php index 8451431..a3c5e32 100644 --- a/examples/books.php +++ b/examples/books.php @@ -1,59 +1,93 @@ __( 'Book', 'post-types' ), + 'singular_name' => __( 'Book', 'post-types' ), + 'menu_name' => __( 'Books', 'post-types' ), + 'all_items' => __( 'Books', 'post-types' ), + 'add_new' => __( 'Add New', 'post-types' ), + 'add_new_item' => __( 'Add New Book', 'post-types' ), + 'edit_item' => __( 'Edit Book', 'post-types' ), + 'new_item' => __( 'New Book', 'post-types' ), + 'view_item' => __( 'View Book', 'post-types' ), + 'search_items' => __( 'Search Books', 'post-types' ), + 'not_found' => __( 'No Books found', 'post-types' ), + 'not_found_in_trash' => __( 'No Books found in Trash', 'post-types'), + 'parent_item_colon' => __( 'Parent Book', 'post-types' ), + ]; + } -// Create a books Post Type -$books = new PostType( 'book' ); + public function taxonomies(): array { + return [ + 'post_tag', + 'genre', + ]; + } -// Add the Genre Taxonomy -$books->taxonomy( 'genre' ); + public function supports(): array { + return [ + 'title', + 'editor', + 'author', + 'custom-fields', + ]; + } -// Hide the date and author columns -$books->columns()->hide( [ 'date', 'author' ] ); + public function options(): array { + return [ + 'show_in_rest' => false, + ]; + } -// add a price and rating column -$books->columns()->add( [ - 'rating' => __( 'Rating' ), - 'price' => __( 'Price' ) -] ); + public function icon(): string { + return 'dashicons-book'; + } -// Populate the custom column -$books->columns()->populate( 'rating', function( $column, $post_id ) { - echo get_post_meta( $post_id, 'rating' ) . '/10'; -} ); + public function filters(): array { + return [ + 'genre', + 'post_tag', + ]; + } -// Populate the custom column -$books->columns()->populate( 'price', function( $column, $post_id ) { - echo '£' . get_post_meta( $post_id, 'price' ); -} ); + public function columns( Columns $columns ): Columns { -// Set sortable columns -$books->columns()->sortable( [ - 'price' => [ 'price', true ], - 'rating' => [ 'rating', true ] -] ); + $columns->remove( [ 'author', 'date' ] ); -// Set the Books menu icon -$books->icon( 'dashicons-book-alt' ); + $columns->column( new Price ); -// Register the PostType to WordPress -$books->register(); + $columns->add( 'rating', __( 'Rating', 'post-types' ) ); -// Create the genre Taxonomy -$genres = new Taxonomy( 'genre' ); + $columns->populate( 'rating', function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ); -// Add a popularity column to the genre taxonomy -$genres->columns()->add( [ - 'popularity' => 'Popularity' -] ); + $columns->sortable( 'rating', function( $query ) { + $query->set('orderby', 'meta_value_num'); + $query->set('meta_key', 'rating'); + } ); -// Populate the new column -$genres->columns()->populate( 'popularity', function( $content, $column, $term_id ) { - return get_term_meta( $term_id, 'popularity', true ); -} ); + $columns->order( [ + 'price' => 4, + 'rating' => 5, + 'taxonomy-genre' => 2, + 'tags' => 3, + ] ); -// Register the taxonomy to WordPress -$genres->register(); + return $columns; + } +} From 00bc920def1cfc01e21af62b84747b3725ed79b0 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Fri, 1 Aug 2025 08:35:26 +0100 Subject: [PATCH 21/41] update post type index and tweaks --- docs/post-types/Creating-columns.md | 40 +++++++++++++++++++++-------- docs/post-types/README.md | 1 + 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/post-types/Creating-columns.md b/docs/post-types/Creating-columns.md index f1fc00e..6561bba 100644 --- a/docs/post-types/Creating-columns.md +++ b/docs/post-types/Creating-columns.md @@ -9,32 +9,56 @@ Columns are defined by extending the abstract `PostTypes\Column` class and imple To create a custom column, extend the base `Column` class and implement the methods you need. Here's an example of a `PriceColumn` that pulls a `_price` meta field from the post and displays it in the admin list table: ```php -namespace App\Columns; - use PostTypes\Column; class PriceColumn extends Column { + /** + * Defines the column key used internally. + * + * @return string. + */ public function name(): string { return 'price'; } + /** + * Define the column label. + * + * @return string + */ public function label(): string { return __( 'Price', 'my-text-domain' ); } + /** + * Populate column callback. + * + * @return void + */ public function populate( int $post_id ): void { echo '$' . get_post_meta( $post_id, '_price', true ); } + /** + * Set the column can be sorted. + * + * @return boolean + */ public function isSortable(): bool { return true; } + /** + * Handle sorting the column by modifying the admin query. + * + * @param $query \WP_Query + * @return void + */ public function sort(\WP_Query $query): void { $query->set( 'meta_key', '_price' ); @@ -43,26 +67,20 @@ class PriceColumn extends Column } ``` -- `name()` defines the column key used internally. -- `label()` sets the visible column heading. -- `populate()` is called when rendering the column for each row. -- `isSortable()` and `sort()` handle sorting logic. - ## Adding the Column to a Post Type -Once you’ve defined your custom column, you can add it to a PostType using the `$columns->add()` method inside your `PostType` class: +Once you’ve defined your custom column, you can add it to a PostType using the `$columns->column()` method inside your `PostType` class: ```php -use App\Columns\PriceColumn; use PostTypes\PostType; class Book extends PostType { //... - public function columns($columns): void + public function columns( $columns ): void { - $columns->add(new PriceColumn); + $columns->column( new PriceColumn ); return $columns; } diff --git a/docs/post-types/README.md b/docs/post-types/README.md index 52f9f3f..1b9c6f4 100644 --- a/docs/post-types/README.md +++ b/docs/post-types/README.md @@ -10,4 +10,5 @@ The following section contains information on creating and working with post typ * [Defining an icon](Defining-an-icon.md) * [Defining filters](Defining-filters.md) * [Modifying columns](Modifying-columns.md) +* [Creating columns](Creating-columns.md) * [Defining hooks](Defining-hooks.md) From bb08bd1d0e23f598d74a7e812c47edb5e29116cf Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 9 Dec 2025 18:55:56 +0000 Subject: [PATCH 22/41] add PHPStan --- composer.json | 15 +- composer.lock | 404 ++++++++++++++++++++++++++++++++++++++-------- phpstan.neon.dist | 6 + 3 files changed, 355 insertions(+), 70 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/composer.json b/composer.json index 011d3a1..78a15e2 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,10 @@ }, "require-dev": { "phpunit/phpunit": "^9.6", - "squizlabs/php_codesniffer": "3.*" + "squizlabs/php_codesniffer": "3.*", + "phpstan/phpstan": "^2.1", + "szepeviktor/phpstan-wordpress": "^2.0", + "phpstan/extension-installer": "^1.4" }, "autoload": { "psr-4": { @@ -30,8 +33,14 @@ "test": [ "./vendor/bin/phpcs --standard=psr2 src", "./vendor/bin/phpunit --coverage-clover=coverage.xml" - ] + ], + "phpstan": "phpstan analyse --memory-limit=512M" }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } + } } diff --git a/composer.lock b/composer.lock index b53ef6a..f3d0f1f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ce0e59647caa2ce5d97d11f9eaad608d", + "content-hash": "6e44aa85049a2eb967278d546b2a0abe", "packages": [], "packages-dev": [ { @@ -79,16 +79,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -127,7 +127,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -135,20 +135,20 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -167,7 +167,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -191,9 +191,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -313,6 +313,158 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.9.0", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.0" + }, + "time": "2025-12-03T23:06:24+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.33", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-12-05T10:24:31+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.32", @@ -634,16 +786,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "945d0b7f346a084ce5549e95289962972c4272e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", + "reference": "945d0b7f346a084ce5549e95289962972c4272e5", "shasum": "" }, "require": { @@ -654,7 +806,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -665,11 +817,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -717,7 +869,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" }, "funding": [ { @@ -728,12 +880,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2025-12-06T07:45:52+00:00" }, { "name": "sebastian/cli-parser", @@ -904,16 +1064,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -966,15 +1126,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -1164,16 +1336,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -1229,28 +1401,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -1293,15 +1477,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -1474,16 +1670,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -1525,15 +1721,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -1700,16 +1908,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.3", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -1726,11 +1934,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -1774,22 +1977,89 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-09-18T10:38:58+00:00" + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "composer/semver": "^3.4", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.3" + }, + "time": "2025-09-14T02:58:22+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -1818,7 +2088,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -1826,17 +2096,17 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=7.2" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..60b34c0 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + level: 5 + paths: + - src + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php From 13416865bbc595cce97c51639a4bf2ebf6a2b2e1 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 9 Dec 2025 18:56:16 +0000 Subject: [PATCH 23/41] fix phpstan errors --- src/Columns.php | 4 ++-- src/Registrars/PostTypeRegistrar.php | 4 ++-- src/Registrars/TaxonomyRegistrar.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Columns.php b/src/Columns.php index cdd7be5..a217dc2 100644 --- a/src/Columns.php +++ b/src/Columns.php @@ -70,7 +70,7 @@ public function column(ColumnContract $column) * @param callable|null $callback * @return void */ - public function add(string $key, string $label, callable $callback = null) + public function add(string $key, string $label, ?callable $callback = null) { $this->add[$key] = $label; @@ -129,7 +129,7 @@ public function sortable(string $key, callable $callback) * Apply columns. * * @param array $columns - * @return void + * @return array */ public function applyColumns(array $columns) { diff --git a/src/Registrars/PostTypeRegistrar.php b/src/Registrars/PostTypeRegistrar.php index 6923693..361e63e 100644 --- a/src/Registrars/PostTypeRegistrar.php +++ b/src/Registrars/PostTypeRegistrar.php @@ -45,11 +45,11 @@ public function register() add_action('init', [$this, 'initialize'], 10, 0); // Handle PostType filters. - add_action('restrict_manage_posts', [$this, 'modifyFilters'], 10, 2); + add_action('restrict_manage_posts', [$this, 'modifyFilters'], 10, 1); // Handle PostType columns. add_filter('manage_' . $name . '_posts_columns', [$this, 'modifyColumns'], 10, 1); - add_filter('manage_' . $name . '_posts_custom_column', [$this, 'populateColumns'], 10, 2); + add_action('manage_' . $name . '_posts_custom_column', [$this, 'populateColumns'], 10, 2); add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); add_action('pre_get_posts', [$this, 'sortSortableColumns'], 10, 1); diff --git a/src/Registrars/TaxonomyRegistrar.php b/src/Registrars/TaxonomyRegistrar.php index 1548c06..23363ba 100644 --- a/src/Registrars/TaxonomyRegistrar.php +++ b/src/Registrars/TaxonomyRegistrar.php @@ -46,7 +46,7 @@ public function register() // Handle Taxonomy columns. add_filter('manage_edit-' . $name . '_columns', [$this, 'modifyColumns'], 10, 1); - add_filter('manage_' . $name . '_custom_column', [$this, 'populateColumns'], 10, 3); + add_action('manage_' . $name . '_custom_column', [$this, 'populateColumns'], 10, 3); add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); add_action('parse_term_query', [$this, 'sortSortableColumns'], 10, 1); @@ -71,7 +71,7 @@ public function createColumns() */ public function registerTaxonomy() { - register_taxonomy($this->taxonomy->name(), null, $this->generateOptions()); + register_taxonomy($this->taxonomy->name(), [], $this->generateOptions()); } /** From 002fa425e5df0600f77ab017257196dcd9bc0e08 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 9 Dec 2025 19:32:29 +0000 Subject: [PATCH 24/41] update workflows --- .github/workflows/tests.yml | 52 +++++++++++++++++++++++++++---------- composer.json | 6 ++--- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f5729e7..ca107f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,15 +1,14 @@ -name: tests +name: Tests on: push: branches: - - master + - main pull_request: - branches: - - master jobs: - build: + phpcs: + name: Coding Standards (PHPCS) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,20 +21,45 @@ jobs: - name: Validate composer.json and composer.lock run: composer validate --strict - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: PHPCS + run: composer phpcs + + phpstan: + name: Static Analysis (PHPStan) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- + php-version: '8.3' + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: PHPStan + run: composer phpstan + + phpunit: + name: Unit Tests (PHPUnit) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' - name: Install dependencies run: composer install --prefer-dist --no-progress - - name: Run test suite - run: composer run-script test + - name: PHPUnit + run: composer phpunit - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/composer.json b/composer.json index 78a15e2..985d10b 100644 --- a/composer.json +++ b/composer.json @@ -30,10 +30,8 @@ } }, "scripts": { - "test": [ - "./vendor/bin/phpcs --standard=psr2 src", - "./vendor/bin/phpunit --coverage-clover=coverage.xml" - ], + "phpcs": "phpcs --standard=psr2 src", + "phpunit": "phpunit --coverage-clover=coverage.xml", "phpstan": "phpstan analyse --memory-limit=512M" }, "minimum-stability": "dev", From 77fb5c5bd0366a7be7044e0adc22d3ee1299a6d8 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 10 Dec 2025 21:59:50 +0000 Subject: [PATCH 25/41] update columns methods --- src/Column.php | 6 +- src/ColumnBuilder.php | 107 ++++++++++++++++ src/Columns.php | 207 ++++++++++++++++++------------- src/Contracts/ColumnContract.php | 8 +- tests/ColumBuilderTest.php | 122 ++++++++++++++++++ tests/ColumnTest.php | 2 +- tests/ColumnsTest.php | 198 +++++++++++++---------------- 7 files changed, 444 insertions(+), 206 deletions(-) create mode 100644 src/ColumnBuilder.php create mode 100644 tests/ColumBuilderTest.php diff --git a/src/Column.php b/src/Column.php index 857c7b0..7bdace6 100644 --- a/src/Column.php +++ b/src/Column.php @@ -37,9 +37,9 @@ public function populate(int $objectId): void /** * Set the column order. * - * @return integer|null + * @return array|null */ - public function order(): ?int + public function position(): ?array { return null; } @@ -50,7 +50,7 @@ public function order(): ?int * @param \WP_Query|\WP_Term_Query $query * @return void */ - public function sort($query) + public function sort($query): void { return; } diff --git a/src/ColumnBuilder.php b/src/ColumnBuilder.php new file mode 100644 index 0000000..be599b5 --- /dev/null +++ b/src/ColumnBuilder.php @@ -0,0 +1,107 @@ +manager = $manager; + $this->key = $key; + } + + /** + * Set the label for the column. + * + * @param string $label + * @return ColumnBuilder + */ + public function label(string $label): ColumnBuilder + { + $this->manager->add($this->key, $label); + + return $this; + } + + /** + * Position a column. + * + * @param string $direction + * @param string $reference + * @return ColumnBuilder + */ + public function position(string $direction, string $reference): ColumnBuilder + { + $this->manager->position($this->key, $direction, $reference); + + return $this; + } + + /** + * Position a column after another. + * + * @param string $reference + * @return ColumnBuilder + */ + public function after(string $reference): ColumnBuilder + { + return $this->position('after', $reference); + } + + /** + * Position a column before another. + * + * @param string $reference + * @return ColumnBuilder + */ + public function before(string $reference): ColumnBuilder + { + return $this->position('before', $reference); + } + + /** + * Set columns populate callback. + * + * @param callable $callback + * @return ColumnBuilder + */ + public function populate(callable $callback): ColumnBuilder + { + $this->manager->populate($this->key, $callback); + + return $this; + } + + /** + * Set columns sort callback. + * + * @param callable $callback + * @return ColumnBuilder + */ + public function sort(callable $callback): ColumnBuilder + { + $this->manager->sort($this->key, $callback); + + return $this; + } +} diff --git a/src/Columns.php b/src/Columns.php index cdd7be5..e442f6b 100644 --- a/src/Columns.php +++ b/src/Columns.php @@ -2,6 +2,7 @@ namespace PostTypes; +use InvalidArgumentException; use PostTypes\Contracts\ColumnContract; class Columns @@ -11,35 +12,42 @@ class Columns * * @var array */ - public $add = []; + protected $columns = []; /** - * Column populate callbacks. + * Columns to remove. * * @var array */ - public $populate = []; + protected $remove = []; /** - * Columns to remove. + * Columns to set. + * + * @var array + */ + protected $only = []; + + /** + * Column positions. * * @var array */ - public $remove = []; + protected $positions = []; /** - * Columns order. + * Column populate callbacks. * * @var array */ - public $order = []; + protected $populateCallbacks = []; /** * Sortable columns and sort callbacks. * * @var array */ - public $sortable = []; + protected $sortCallbacks = []; /** * Add a column object. @@ -47,48 +55,55 @@ class Columns * @param ColumnContract $column * @return void */ - public function column(ColumnContract $column) + public function column(ColumnContract $column): void { $this->add($column->name(), $column->label()); $this->populate($column->name(), [$column, 'populate']); - if (!is_null($column->order())) { - $this->order[$column->name()] = $column->order(); + if (!is_null($column->position())) { + [$direction, $reference] = $column->position(); + + $this->position($column->name(), $direction, $reference); } if ($column->isSortable()) { - $this->sortable($column->name(), [$column, 'sort']); + $this->sort($column->name(), [$column, 'sort']); } } /** - * Add a column. + * Create a new Column. * * @param string $key - * @param string $label - * @param callable|null $callback - * @return void + * @return ColumnBuilder */ - public function add(string $key, string $label, callable $callback = null) + public function create(string $key): ColumnBuilder { - $this->add[$key] = $label; + return new ColumnBuilder($this, $key); + } - if (is_callable($callback)) { - $this->populate($key, $callback); - } + /** + * Modify an existing column. + * + * @param string $key + * @return ColumnBuilder + */ + public function modify(string $key): ColumnBuilder + { + return $this->create($key); } /** - * Set column populate callback. + * Add a column. * * @param string $key - * @param callable $callback + * @param string $label * @return void */ - public function populate(string $key, callable $callback) + public function add(string $key, string $label): void { - $this->populate[$key] = $callback; + $this->columns[$key] = $label; } /** @@ -97,115 +112,133 @@ public function populate(string $key, callable $callback) * @param array $keys * @return void */ - public function remove(array $keys) + public function remove(array $keys): void { $this->remove = array_merge($this->remove, $keys); } /** - * Set columns order + * Set columns. * - * @param array $order + * @param array $keys * @return void */ - public function order(array $order) + public function only(array $keys): void { - $this->order = array_merge($this->order, $order); + $this->only = array_merge($this->only, $keys); } /** - * Set sortable columns and sort callback. + * Set column position. * * @param string $key - * @param callable $callback + * @param string $direction + * @param string $reference * @return void + * @throws InvalidArgumentException */ - public function sortable(string $key, callable $callback) + public function position(string $key, string $direction, string $reference): void { - $this->sortable[$key] = $callback; + if (!in_array($direction, ['before', 'after'], true)) { + throw new InvalidArgumentException("Invalid position direction '{$direction}'"); + } + + $this->positions[$key] = [$direction, $reference]; } /** - * Apply columns. + * Set column populate callback. * - * @param array $columns + * @param string $key + * @param callable $callback * @return void */ - public function applyColumns(array $columns) + public function populate(string $key, callable $callback): void { - if (!empty($this->add)) { - $columns = array_merge($columns, $this->add); - } - - if (!empty($this->remove)) { - $columns = array_diff_key($columns, array_flip($this->remove)); - } - - if (!empty($this->order)) { - $order = $this->order; - - // Sort the order array. - asort($order); - - // Flip order so the index is the position. - $order = array_flip($order); + $this->populateCallbacks[$key] = $callback; + } - // Create the current order array. - $current = array_keys($columns); + /** + * Set sortable columns and sort callback. + * + * @param string $key + * @param callable $callback + * @return void + */ + public function sort(string $key, callable $callback): void + { + $this->sortCallbacks[$key] = $callback; + } - // Loop over the order. - foreach ($order as $index => $key) { - array_splice($current, $index, 0, $key); - } + /** + * Get columns to add. + * + * @return array + */ + public function getColumns(): array + { + return $this->columns; + } - $new = array_flip(array_unique($current)); + /** + * Get removed columns. + * + * @return array + */ + public function getRemoved(): array + { + return $this->remove; + } - $columns = array_merge($new, $columns); - } + /** + * Get only columns. + * + * @return array + */ + public function getOnly(): array + { + return $this->only; + } - return $columns; + /** + * Get column positions. + * + * @return array + */ + public function getPositions(): array + { + return $this->positions; } /** - * Populate a column. + * Get a column populate callback. * - * @param string $column - * @param array $params - * @return void + * @param string $key + * @return callable|null */ - public function populateColumn(string $column, array $params) + public function getPopulateCallback(string $key): ?callable { - if (isset($this->populate[$column]) && is_callable($this->populate[$column])) { - call_user_func_array($this->populate[$column], $params); - } + return $this->populateCallbacks[$key] ?? null; } /** - * Set sortable columns + * Get sortable columns. * - * @param array $columns * @return array */ - public function setSortable(array $columns): array + public function getSortableColumns(): array { - foreach (array_keys($this->sortable) as $key) { - $columns[$key] = $key; - } - - return $columns; + return array_combine(array_keys($this->sortCallbacks), array_keys($this->sortCallbacks)); } /** - * Sort a column. + * Get column sort callback. * - * @param string $column - * @param \WP_Query|\WP_Term_Query $query - * @return void + * @param string $key + * @return callable|null */ - public function sortColumn(string $column, $query) + public function getSortCallback(string $key): ?callable { - if (isset($this->sortable[$column]) && is_callable($this->sortable[$column])) { - call_user_func_array($this->sortable[$column], [$query]); - } + return $this->sortCallbacks[$key] ?? null; } } diff --git a/src/Contracts/ColumnContract.php b/src/Contracts/ColumnContract.php index 45c955e..73edd25 100644 --- a/src/Contracts/ColumnContract.php +++ b/src/Contracts/ColumnContract.php @@ -27,11 +27,11 @@ public function label(): string; public function populate(int $objectId): void; /** - * Set the column order. + * Set the column position. * - * @return integer|null + * @return array|null */ - public function order(): ?int; + public function position(): ?array; /** * Handle sorting the column. @@ -39,7 +39,7 @@ public function order(): ?int; * @param \WP_Query|\WP_Term_Query $query * @return void */ - public function sort($query); + public function sort($query): void; /** * Can the column be sorted. diff --git a/tests/ColumBuilderTest.php b/tests/ColumBuilderTest.php new file mode 100644 index 0000000..e5b3558 --- /dev/null +++ b/tests/ColumBuilderTest.php @@ -0,0 +1,122 @@ +createMock(Columns::class); + + $manager->expects($this->once()) + ->method('add') + ->with('price', 'Price Label'); + + $builder = new ColumnBuilder($manager, 'price'); + + $result = $builder->label('Price Label'); + + $this->assertSame($builder, $result); // fluent + } + + public function test_position_sets_position_correctly() + { + $manager = $this->createMock(Columns::class); + + $manager->expects($this->once()) + ->method('position') + ->with('price', 'after', 'title'); + + $builder = new ColumnBuilder($manager, 'price'); + + $result = $builder->position('after', 'title'); + + $this->assertSame($builder, $result); + } + + public function test_after_sets_position_after_reference() + { + $manager = $this->createMock(Columns::class); + + $manager->expects($this->once()) + ->method('position') + ->with('price', 'after', 'title'); + + $builder = new ColumnBuilder($manager, 'price'); + + $result = $builder->after('title'); + + $this->assertSame($builder, $result); + } + + public function test_before_sets_position_before_reference() + { + $manager = $this->createMock(Columns::class); + + $manager->expects($this->once()) + ->method('position') + ->with('price', 'before', 'title'); + + $builder = new ColumnBuilder($manager, 'price'); + + $result = $builder->before('title'); + + $this->assertSame($builder, $result); + } + + public function test_populate_sets_populate_callback() + { + $callback = function () {}; + + $manager = $this->createMock(Columns::class); + + $manager->expects($this->once()) + ->method('populate') + ->with('price', $callback); + + $builder = new ColumnBuilder($manager, 'price'); + + $result = $builder->populate($callback); + + $this->assertSame($builder, $result); + } + + public function test_sort_sets_sort_callback() + { + $callback = function () {}; + + $manager = $this->createMock(Columns::class); + + $manager->expects($this->once()) + ->method('sort') + ->with('price', $callback); + + $builder = new ColumnBuilder($manager, 'price'); + + $result = $builder->sort($callback); + + $this->assertSame($builder, $result); + } + + public function test_builder_fluency_all_methods_chain() + { + $manager = $this->createMock(Columns::class); + + $manager->expects($this->once())->method('add')->with('price', 'Price'); + $manager->expects($this->once())->method('position')->with('price', 'after', 'title'); + $manager->expects($this->once())->method('populate'); + $manager->expects($this->once())->method('sort'); + + $builder = new ColumnBuilder($manager, 'price'); + + $result = $builder + ->label('Price') + ->after('title') + ->populate(function () {}) + ->sort(function () {}); + + $this->assertSame($builder, $result); + } +} diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php index 4bafa1a..ab7e5bd 100644 --- a/tests/ColumnTest.php +++ b/tests/ColumnTest.php @@ -16,7 +16,7 @@ public function test_column_returns_defaults() $this->assertEquals('price', $stub->name()); $this->assertEquals('Price', $stub->label()); $this->assertEquals(null, $stub->populate(1)); - $this->assertEquals(null, $stub->order()); + $this->assertEquals(null, $stub->position()); $this->assertEquals(null, $stub->sort(true)); $this->assertEquals(false, $stub->isSortable()); } diff --git a/tests/ColumnsTest.php b/tests/ColumnsTest.php index 7500015..b0818ad 100644 --- a/tests/ColumnsTest.php +++ b/tests/ColumnsTest.php @@ -1,8 +1,9 @@ add('column', 'Test Column'); - $this->assertArrayHasKey('column', $columns->add); - $this->assertSame('Test Column', $columns->add['column']); + $output = $columns->getColumns(); + + $this->assertArrayHasKey('column', $output); + $this->assertSame('Test Column', $output['column']); } public function test_can_add_column_with_column_class() { - $stub = $this->getMockForAbstractClass(Column::class); + $stub = $this->createMock(ColumnContract::class); - $stub->expects($this->any()) - ->method('name') - ->will($this->returnValue('column')); + $stub->method('name')->willReturn('column'); + $stub->method('label')->willReturn('Column'); + $stub->method('position')->willReturn(['after', 'title']); + $stub->method('isSortable')->willReturn(true); + $stub->method('sort')->willReturnCallback(function () {}); $columns = new Columns; $columns->column($stub); - $this->assertArrayHasKey('column', $columns->add); - $this->assertSame('Column', $columns->add['column']); + $output = $columns->getColumns(); + $positions = $columns->getPositions(); + $sortable = $columns->getSortCallback('column'); + + $this->assertArrayHasKey('column', $output); + $this->assertSame('Column', $output['column']); + + $this->assertArrayHasKey('column', $positions); + $this->assertSame(['after', 'title'], $positions['column']); + + $this->assertIsCallable($sortable); + } + + public function test_create_returns_column_builder() + { + $columns = new Columns; - $this->assertArrayHasKey('column', $columns->populate); - $this->assertIsCallable($columns->populate['column']); + $builder = $columns->create('new_column'); + + $this->assertInstanceOf(ColumnBuilder::class, $builder); } - public function test_can_add_column_with_populate_callback() + public function test_modify_returns_column_builder() { $columns = new Columns; - $columns->add('column', 'Test Column', function() {}); + $builder = $columns->modify('existing'); - $this->assertArrayHasKey('column', $columns->populate); - $this->assertIsCallable($columns->populate['column']); + $this->assertInstanceOf(ColumnBuilder::class, $builder); } public function test_can_set_column_populate_callback() { $columns = new Columns; - $columns->populate('column', function() {}); + $callback = function () {}; + $columns->populate('column', $callback); + + $this->assertSame($callback, $columns->getPopulateCallback('column')); + } - $this->assertArrayHasKey('column', $columns->populate); - $this->assertIsCallable($columns->populate['column']); + public function test_get_populate_callback_returns_null_for_missing_key() + { + $columns = new Columns; + + $this->assertNull($columns->getPopulateCallback('missing')); } public function test_can_set_remove_column() @@ -60,7 +86,7 @@ public function test_can_set_remove_column() $columns->remove(['column']); - $this->assertEquals(['column'], $columns->remove); + $this->assertEquals(['column'], $columns->getRemoved()); } public function test_can_set_remove_columns_with_multiple_calls() @@ -70,150 +96,100 @@ public function test_can_set_remove_columns_with_multiple_calls() $columns->remove(['column']); $columns->remove(['column_2']); - $this->assertEquals(['column', 'column_2'], $columns->remove); + $this->assertEquals(['column', 'column_2'], $columns->getRemoved()); } - public function test_can_set_order_columns() + public function test_can_set_only_columns() { $columns = new Columns; - $columns->order([ - 'column' => 1, - ]); + $columns->only(['one']); + $columns->only(['two']); - $this->assertEquals(['column' => 1], $columns->order); + $this->assertEquals(['one', 'two'], $columns->getOnly()); } - public function test_can_set_order_columns_with_multiple_calls() + public function test_can_set_position_after() { $columns = new Columns; - $columns->order(['column' => 1]); - $columns->order(['column_2' => 3]); + $columns->position('col', 'after', 'title'); - $this->assertEquals(['column' => 1, 'column_2' => 3], $columns->order); + $this->assertSame(['after', 'title'], $columns->getPositions()['col']); } - public function test_can_order_column_with_column_class() + public function test_can_set_position_before() { $columns = new Columns; - $stub = $this->createMock(Column::class); - - $stub->expects($this->any()) - ->method('name') - ->will($this->returnValue('column')); - - $stub->expects($this->any()) - ->method('order') - ->will($this->returnValue(1)); + $columns->position('col', 'before', 'date'); + $this->assertSame(['before', 'date'], $columns->getPositions()['col']); + } - $columns->column($stub); + public function test_position_throws_exception_for_invalid_direction() + { + $this->expectException(InvalidArgumentException::class); - $this->assertEquals(['column' => 1], $columns->order); + $columns = new Columns; + $columns->position('col', 'sideways', 'title'); } public function test_can_set_sortable_column() { $columns = new Columns; - $columns->sortable('column', function() {}); + $callback = function () {}; + $columns->sort('column', $callback); - $this->assertArrayHasKey('column', $columns->sortable); - $this->assertIsCallable($columns->sortable['column']); + $this->assertSame($callback, $columns->getSortCallback('column')); } - public function test_can_apply_columns() + public function test_get_sortable_columns_returns_keys_mapped_to_keys() { $columns = new Columns; - $columns->add('column_5', 'Column 5'); + $columns->sort('a', function () {}); + $columns->sort('b', function () {}); - $columns->remove(['column_2']); - - $columns->order([ - 'column_3' => 0, - ]); - - $original = [ - 'column_1' => 'Column 1', - 'column_2' => 'Column 2', - 'column_3' => 'Column 3', - 'column_4' => 'Column 4', - ]; - - $modified = $columns->applyColumns($original); - - $expected = [ - 'column_3' => 'Column 3', - 'column_1' => 'Column 1', - 'column_4' => 'Column 4', - 'column_5' => 'Column 5', - ]; - - $this->assertSame($expected, $modified); + $this->assertSame(['a' => 'a', 'b' => 'b'], $columns->getSortableColumns()); } - public function test_can_populate_column() + public function test_get_sortable_callback_returns_null_for_missing_key() { $columns = new Columns; - $stub = $this->createMock(Column::class); - - $stub->expects($this->any()) - ->method('name') - ->will($this->returnValue('column')); - - $stub->expects($this->once()) - ->method('populate') - ->with($this->greaterThan(0)); - - $columns->column($stub); - - $columns->populateColumn('column', [1]); + $this->assertNull($columns->getSortCallback('missing')); } - public function test_can_add_sortable_columns_to_sortable_list() + public function test_populate_does_not_affect_sort_callbacks() { $columns = new Columns; - $columns->sortable('column', function() {}); - - $sortable = [ - 'title' => 'title', - ]; - - $sortable = $columns->setSortable($sortable); + $columns->populate('col', function () {}); - $expected = [ - 'title' => 'title', - 'column' => 'column', - ]; - - $this->assertSame($expected, $sortable); + $this->assertNull($columns->getSortCallback('col')); } - public function test_can_sort_column() + public function test_sort_does_not_affect_populate_callbacks() { $columns = new Columns; - $stub = $this->createMock(Column::class); - - $stub->expects($this->any()) - ->method('name') - ->will($this->returnValue('column')); + $columns->sort('col', function () {}); - $stub->expects($this->once()) - ->method('isSortable') - ->will($this->returnValue(true)); + $this->assertNull($columns->getPopulateCallback('col')); + } - $stub->expects($this->once()) - ->method('sort') - ->with($this->greaterThan(0)); + public function test_add_does_not_overwrite_callback_data() + { + $columns = new Columns; - $columns->column($stub); + $columns->populate('col', function () {}); + $columns->sort('col', function () {}); + $columns->add('col', 'Label'); - $columns->sortColumn('column', 1); + // Callbacks remain unchanged + $this->assertIsCallable($columns->getPopulateCallback('col')); + $this->assertIsCallable($columns->getSortCallback('col')); } } From 57a178ac9c69e1b7a283f6492345bf62d11931b8 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 10 Dec 2025 22:00:12 +0000 Subject: [PATCH 26/41] update columns integration --- src/Registrars/PostTypeRegistrar.php | 57 +++++++++++++++++++--- src/Registrars/TaxonomyRegistrar.php | 52 ++++++++++++++++++-- tests/Registrars/PostTypeRegistrarTest.php | 2 +- tests/Registrars/TaxonomyRegistrarTest.php | 2 +- 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/Registrars/PostTypeRegistrar.php b/src/Registrars/PostTypeRegistrar.php index 6923693..2a5703b 100644 --- a/src/Registrars/PostTypeRegistrar.php +++ b/src/Registrars/PostTypeRegistrar.php @@ -45,11 +45,11 @@ public function register() add_action('init', [$this, 'initialize'], 10, 0); // Handle PostType filters. - add_action('restrict_manage_posts', [$this, 'modifyFilters'], 10, 2); + add_action('restrict_manage_posts', [$this, 'modifyFilters'], 10, 1); // Handle PostType columns. add_filter('manage_' . $name . '_posts_columns', [$this, 'modifyColumns'], 10, 1); - add_filter('manage_' . $name . '_posts_custom_column', [$this, 'populateColumns'], 10, 2); + add_action('manage_' . $name . '_posts_custom_column', [$this, 'populateColumns'], 10, 2); add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); add_action('pre_get_posts', [$this, 'sortSortableColumns'], 10, 1); @@ -171,7 +171,43 @@ public function modifyFilters($posttype) */ public function modifyColumns(array $columns) { - return $this->columns->applyColumns($columns); + foreach ($this->columns->getColumns() as $key => $label) { + $columns[$key] = $label; + } + + if ($remove = $this->columns->getRemoved()) { + $columns = array_diff_key($columns, array_flip($remove)); + } + + if ($only = $this->columns->getOnly()) { + $columns = array_intersect_key($columns, array_flip($only)); + } + + foreach ($this->columns->getPositions() as $key => $position) { + [$direction, $reference] = $position; + + if (!isset($direction) || !isset($reference)) { + continue; + } + + $new = []; + + foreach ($columns as $k => $label) { + if ('before' === $direction && $k === $reference) { + $new[$key] = $columns[$key]; + } + + $new[$k] = $label; + + if ('after' === $direction && $k === $reference) { + $new[$key] = $columns[$key]; + } + } + + $columns = $new; + } + + return $columns; } /** @@ -183,7 +219,11 @@ public function modifyColumns(array $columns) */ public function populateColumns($column, $post_id) { - $this->columns->populateColumn($column, [$post_id]); + $callback = $this->columns->getPopulateCallback($column); + + if ($callback) { + call_user_func_array($callback, [$post_id]); + } } /** @@ -194,7 +234,9 @@ public function populateColumns($column, $post_id) */ public function setSortableColumns($columns) { - return $this->columns->setSortable($columns); + $sortable = $this->columns->getSortableColumns(); + + return array_merge($columns, $sortable); } /** @@ -210,7 +252,10 @@ public function sortSortableColumns($query) } $column = $query->get('orderby'); + $callback = $this->columns->getSortCallback($column); - $this->columns->sortColumn($column, $query); + if ($callback) { + call_user_func_array($callback, [$query]); + } } } diff --git a/src/Registrars/TaxonomyRegistrar.php b/src/Registrars/TaxonomyRegistrar.php index 1548c06..ec04262 100644 --- a/src/Registrars/TaxonomyRegistrar.php +++ b/src/Registrars/TaxonomyRegistrar.php @@ -115,7 +115,44 @@ public function registerTaxonomyToPostTypes() */ public function modifyColumns(array $columns) { - return $this->columns->applyColumns($columns); + foreach ($this->columns->getColumns() as $key => $label) { + $columns[$key] = $label; + } + + if ($remove = $this->columns->getRemoved()) { + $columns = array_diff_key($columns, array_flip($remove)); + } + + if ($only = $this->columns->getOnly()) { + $columns = array_intersect_key($columns, array_flip($only)); + } + + + foreach ($this->columns->getPositions() as $key => $position) { + [$direction, $reference] = $position; + + if (!isset($direction) || !isset($reference)) { + continue; + } + + $new = []; + + foreach ($columns as $k => $label) { + if ('before' === $direction && $k === $reference) { + $new[$key] = $columns[$key]; + } + + $new[$k] = $label; + + if ('after' === $direction && $k === $reference) { + $new[$key] = $columns[$key]; + } + } + + $columns = $new; + } + + return $columns; } /** @@ -128,7 +165,11 @@ public function modifyColumns(array $columns) */ public function populateColumns($content, $column, $term_id) { - $this->columns->populateColumn($column, [$term_id, $content]); + $callback = $this->columns->getPopulateCallback($column); + + if ($callback) { + call_user_func_array($callback, [$term_id, $content]); + } } /** @@ -139,7 +180,7 @@ public function populateColumns($content, $column, $term_id) */ public function setSortableColumns($columns) { - return $this->columns->setSortable($columns); + return array_merge($columns, $this->columns->getSortableColumns()); } /** @@ -155,7 +196,10 @@ public function sortSortableColumns($query) } $column = $query->query_vars['orderby']; + $callback = $this->columns->getSortCallback($column); - $this->columns->sortColumn($column, $query); + if ($callback) { + call_user_func_array($callback, [$query]); + } } } diff --git a/tests/Registrars/PostTypeRegistrarTest.php b/tests/Registrars/PostTypeRegistrarTest.php index eb480e2..adcc4ea 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -142,7 +142,7 @@ public function test_can_populate_column() public function test_can_set_sortable_columns() { $columns = new Columns; - $columns->sortable('column', function() {}); + $columns->sort('column', function() {}); $sortable = [ 'title' => 'title', diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index 15e62a9..47d0862 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -129,7 +129,7 @@ public function test_can_populate_column() public function test_can_set_sortable_columns() { $columns = new Columns; - $columns->sortable('column', function() {}); + $columns->sort('column', function() {}); $sortable = [ 'title' => 'title', From e9bd01d13644dc0aff16611d0b322b79c0b79fe3 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 16 Dec 2025 13:59:33 +0000 Subject: [PATCH 27/41] update columns method names --- src/Columns.php | 68 +++++++++++----------- tests/ColumnsTest.php | 8 +-- tests/Registrars/PostTypeRegistrarTest.php | 2 +- tests/Registrars/TaxonomyRegistrarTest.php | 2 +- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Columns.php b/src/Columns.php index e442f6b..82c39fa 100644 --- a/src/Columns.php +++ b/src/Columns.php @@ -8,11 +8,11 @@ class Columns { /** - * Columns to add. + * Columns keys and labels. * * @var array */ - protected $columns = []; + protected $labels = []; /** * Columns to remove. @@ -22,7 +22,7 @@ class Columns protected $remove = []; /** - * Columns to set. + * Columns whitelist. * * @var array */ @@ -49,36 +49,13 @@ class Columns */ protected $sortCallbacks = []; - /** - * Add a column object. - * - * @param ColumnContract $column - * @return void - */ - public function column(ColumnContract $column): void - { - $this->add($column->name(), $column->label()); - - $this->populate($column->name(), [$column, 'populate']); - - if (!is_null($column->position())) { - [$direction, $reference] = $column->position(); - - $this->position($column->name(), $direction, $reference); - } - - if ($column->isSortable()) { - $this->sort($column->name(), [$column, 'sort']); - } - } - /** * Create a new Column. * * @param string $key * @return ColumnBuilder */ - public function create(string $key): ColumnBuilder + public function add(string $key): ColumnBuilder { return new ColumnBuilder($this, $key); } @@ -91,19 +68,30 @@ public function create(string $key): ColumnBuilder */ public function modify(string $key): ColumnBuilder { - return $this->create($key); + return $this->add($key); } /** - * Add a column. + * Add a column object. * - * @param string $key - * @param string $label + * @param ColumnContract $column * @return void */ - public function add(string $key, string $label): void + public function column(ColumnContract $column): void { - $this->columns[$key] = $label; + $this->label($column->name(), $column->label()); + + $this->populate($column->name(), [$column, 'populate']); + + if (!is_null($column->position())) { + [$direction, $reference] = $column->position(); + + $this->position($column->name(), $direction, $reference); + } + + if ($column->isSortable()) { + $this->sort($column->name(), [$column, 'sort']); + } } /** @@ -128,6 +116,18 @@ public function only(array $keys): void $this->only = array_merge($this->only, $keys); } + /** + * Set the label for a column. + * + * @param string $key + * @param string $label + * @return void + */ + public function label(string $key, string $label): void + { + $this->labels[$key] = $label; + } + /** * Set column position. * @@ -177,7 +177,7 @@ public function sort(string $key, callable $callback): void */ public function getColumns(): array { - return $this->columns; + return $this->labels; } /** diff --git a/tests/ColumnsTest.php b/tests/ColumnsTest.php index b0818ad..6e0ae0f 100644 --- a/tests/ColumnsTest.php +++ b/tests/ColumnsTest.php @@ -7,11 +7,11 @@ class ColumnsTest extends TestCase { - public function test_can_add_column() + public function test_can_label_column() { $columns = new Columns; - $columns->add('column', 'Test Column'); + $columns->label('column', 'Test Column'); $output = $columns->getColumns(); @@ -45,11 +45,11 @@ public function test_can_add_column_with_column_class() $this->assertIsCallable($sortable); } - public function test_create_returns_column_builder() + public function test_add_returns_column_builder() { $columns = new Columns; - $builder = $columns->create('new_column'); + $builder = $columns->add('new_column'); $this->assertInstanceOf(ColumnBuilder::class, $builder); } diff --git a/tests/Registrars/PostTypeRegistrarTest.php b/tests/Registrars/PostTypeRegistrarTest.php index adcc4ea..5dd0658 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -80,7 +80,7 @@ public function test_can_modify_columns() ]; $columns = new Columns; - $columns->add('date', 'Date', function() {}); + $columns->label('date', 'Date'); $stub = $this->getMockBuilder(PostType::class) ->getMock(); diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index 47d0862..a341a4f 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -68,7 +68,7 @@ public function test_can_modify_columns() ]; $columns = new Columns; - $columns->add('popularity', 'Popularity', function() {}); + $columns->label('popularity', 'Popularity'); $stub = $this->getMockBuilder(Taxonomy::class) ->getMock(); From 23baeb3d4e2197e2a5fef5ab104c2255d0a32081 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 16 Dec 2025 15:17:29 +0000 Subject: [PATCH 28/41] update column method and integrations --- src/Column.php | 36 ++++++++++++++-------- src/Columns.php | 10 +++--- src/Contracts/ColumnContract.php | 21 ++++--------- src/Registrars/TaxonomyRegistrar.php | 1 - tests/ColumnTest.php | 5 ++- tests/ColumnsTest.php | 6 ++-- tests/Registrars/PostTypeRegistrarTest.php | 2 +- tests/Registrars/TaxonomyRegistrarTest.php | 2 +- 8 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/Column.php b/src/Column.php index 7bdace6..662d580 100644 --- a/src/Column.php +++ b/src/Column.php @@ -26,12 +26,11 @@ public function label(): string /** * Populate the column. * - * @param integer $objectId - * @return void + * @return callable|bull */ - public function populate(int $objectId): void + public function populate(): ?callable { - return; + return null; } /** @@ -45,23 +44,34 @@ public function position(): ?array } /** - * Handle sorting the column. + * Return the sort callback for the column. + * + * @return callable|null + */ + public function sort(): ?callable + { + return null; + } + + /** + * Return the before position array structure. * - * @param \WP_Query|\WP_Term_Query $query - * @return void + * @param string $reference + * @return array */ - public function sort($query): void + protected function before(string $reference): array { - return; + return ['before', $reference]; } /** - * Can the column be sorted. + * Return the after position array structure. * - * @return boolean + * @param string $reference + * @return array */ - public function isSortable(): bool + protected function after(string $reference): array { - return false; + return ['after', $reference]; } } diff --git a/src/Columns.php b/src/Columns.php index 82c39fa..709622d 100644 --- a/src/Columns.php +++ b/src/Columns.php @@ -81,16 +81,18 @@ public function column(ColumnContract $column): void { $this->label($column->name(), $column->label()); - $this->populate($column->name(), [$column, 'populate']); - if (!is_null($column->position())) { [$direction, $reference] = $column->position(); $this->position($column->name(), $direction, $reference); } - if ($column->isSortable()) { - $this->sort($column->name(), [$column, 'sort']); + if ($callback = $column->populate()) { + $this->populate($column->name(), $callback); + } + + if ($callback = $column->sort()) { + $this->sort($column->name(), $callback); } } diff --git a/src/Contracts/ColumnContract.php b/src/Contracts/ColumnContract.php index 73edd25..12fa9c1 100644 --- a/src/Contracts/ColumnContract.php +++ b/src/Contracts/ColumnContract.php @@ -18,14 +18,6 @@ public function name(): string; */ public function label(): string; - /** - * Populate the column. - * - * @param integer $objectId - * @return void - */ - public function populate(int $objectId): void; - /** * Set the column position. * @@ -34,17 +26,16 @@ public function populate(int $objectId): void; public function position(): ?array; /** - * Handle sorting the column. + * Populate the column. * - * @param \WP_Query|\WP_Term_Query $query - * @return void + * @return callable|null */ - public function sort($query): void; + public function populate(): ?callable; /** - * Can the column be sorted. + * Handle sorting the column. * - * @return boolean + * @return callable|null */ - public function isSortable(): bool; + public function sort(): ?callable; } diff --git a/src/Registrars/TaxonomyRegistrar.php b/src/Registrars/TaxonomyRegistrar.php index ec04262..d6244e8 100644 --- a/src/Registrars/TaxonomyRegistrar.php +++ b/src/Registrars/TaxonomyRegistrar.php @@ -127,7 +127,6 @@ public function modifyColumns(array $columns) $columns = array_intersect_key($columns, array_flip($only)); } - foreach ($this->columns->getPositions() as $key => $position) { [$direction, $reference] = $position; diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php index ab7e5bd..bc86be8 100644 --- a/tests/ColumnTest.php +++ b/tests/ColumnTest.php @@ -15,9 +15,8 @@ public function test_column_returns_defaults() $this->assertEquals('price', $stub->name()); $this->assertEquals('Price', $stub->label()); - $this->assertEquals(null, $stub->populate(1)); + $this->assertEquals(null, $stub->populate()); $this->assertEquals(null, $stub->position()); - $this->assertEquals(null, $stub->sort(true)); - $this->assertEquals(false, $stub->isSortable()); + $this->assertEquals(null, $stub->sort()); } } diff --git a/tests/ColumnsTest.php b/tests/ColumnsTest.php index 6e0ae0f..aa6ede4 100644 --- a/tests/ColumnsTest.php +++ b/tests/ColumnsTest.php @@ -26,14 +26,15 @@ public function test_can_add_column_with_column_class() $stub->method('name')->willReturn('column'); $stub->method('label')->willReturn('Column'); $stub->method('position')->willReturn(['after', 'title']); - $stub->method('isSortable')->willReturn(true); - $stub->method('sort')->willReturnCallback(function () {}); + $stub->method('populate')->willReturn(function () {}); + $stub->method('sort')->willReturn(function () {}); $columns = new Columns; $columns->column($stub); $output = $columns->getColumns(); $positions = $columns->getPositions(); + $populate = $columns->getPopulateCallback('column'); $sortable = $columns->getSortCallback('column'); $this->assertArrayHasKey('column', $output); @@ -42,6 +43,7 @@ public function test_can_add_column_with_column_class() $this->assertArrayHasKey('column', $positions); $this->assertSame(['after', 'title'], $positions['column']); + $this->assertIsCallable($populate); $this->assertIsCallable($sortable); } diff --git a/tests/Registrars/PostTypeRegistrarTest.php b/tests/Registrars/PostTypeRegistrarTest.php index 5dd0658..0ac3db1 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -119,7 +119,7 @@ public function test_can_populate_column() $stub->expects($this->once()) ->method('populate') - ->will($this->returnValue(true)); + ->willReturnCallback(function() {}); $columns->column($stub); diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index a341a4f..697b0f7 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -106,7 +106,7 @@ public function test_can_populate_column() $stub->expects($this->once()) ->method('populate') - ->will($this->returnValue(true)); + ->willReturnCallback(function() {}); $columns->column($stub); From 8361735ee30081497e87cdfb878d325faaa4f63f Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 16 Dec 2025 15:17:35 +0000 Subject: [PATCH 29/41] update examples --- examples/Genres.php | 18 ++++++------------ examples/Price.php | 22 +++++++++++----------- examples/books.php | 20 ++++++++++++-------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/examples/Genres.php b/examples/Genres.php index 5086a7a..1d46939 100644 --- a/examples/Genres.php +++ b/examples/Genres.php @@ -20,21 +20,15 @@ public function posttypes(): array { } public function columns( Columns $columns ): Columns { - $columns->remove(['posts']); + $columns->remove( [ 'posts' ] ); - $columns->add( - 'popularity', - __( 'Popularity', 'post-types' ), - function( $term_id ) { - echo get_term_meta( $term_id, 'popularity', true ); - } - ); + $columns->label( 'popularity', __( 'Popularity', 'post-types' ) ); - $columns->order( [ - 'popularity' => 2, - ] ); + $columns->populate( 'popularity', function( $term_id ) { + echo get_term_meta( $term_id, 'popularity', true ); + } ); - $columns->sortable( 'popularity', function( $query ) { + $columns->sort( 'popularity', function( $query ) { $query->query_vars['orderby'] = 'meta_value'; $query->query_vars['meta_key'] = 'popularity'; } ); diff --git a/examples/Price.php b/examples/Price.php index 6246a79..f161c1e 100644 --- a/examples/Price.php +++ b/examples/Price.php @@ -12,20 +12,20 @@ public function label(): string { return __( 'Price', 'post-types' ); } - public function order(): int { - return 2; + public function position(): array { + return $this->after( 'title' ); } - public function populate( int $post_id ): void { - echo '£' . get_post_meta( $post_id, 'price', true ); + public function populate(): callable { + return function( int $post_id ) { + echo '£' . get_post_meta( $post_id, 'price', true ); + }; } - public function isSortable(): bool { - return true; - } - - public function sort( $query ): void { - $query->set('orderby', 'meta_value_num'); - $query->set('meta_key', 'price'); + public function sort(): callable { + return function( $query ) { + $query->set( 'orderby', 'meta_value_num' ); + $query->set( 'meta_key', 'price' ); + }; } } diff --git a/examples/books.php b/examples/books.php index a3c5e32..4fb1851 100644 --- a/examples/books.php +++ b/examples/books.php @@ -70,23 +70,27 @@ public function columns( Columns $columns ): Columns { $columns->column( new Price ); - $columns->add( 'rating', __( 'Rating', 'post-types' ) ); + $columns->label( 'rating', __( 'Rating', 'post-types' ) ); $columns->populate( 'rating', function( $post_id ) { echo get_post_meta( $post_id, 'rating', true ); } ); - $columns->sortable( 'rating', function( $query ) { + $columns->sort( 'rating', function( $query ) { $query->set('orderby', 'meta_value_num'); $query->set('meta_key', 'rating'); } ); - $columns->order( [ - 'price' => 4, - 'rating' => 5, - 'taxonomy-genre' => 2, - 'tags' => 3, - ] ); + $columns->add( 'rating' ) + ->after( 'price' ) + ->label( __( 'Rating', 'post-types' ) ) + ->populate( function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ) + ->sort( function( $query ) { + $query->set('orderby', 'meta_value_num'); + $query->set('meta_key', 'rating'); + } ); return $columns; } From ba2605aa3fb3ac65a56969db80d779b2aa71c819 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Tue, 16 Dec 2025 18:53:26 +0000 Subject: [PATCH 30/41] update documentation for columns --- docs/post-types/Creating-columns.md | 29 +++++++++++++++------------- docs/post-types/Modifying-columns.md | 15 ++++++-------- docs/taxonomies/Modifying-columns.md | 8 +++----- examples/books.php | 2 ++ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/post-types/Creating-columns.md b/docs/post-types/Creating-columns.md index 6561bba..ba9f030 100644 --- a/docs/post-types/Creating-columns.md +++ b/docs/post-types/Creating-columns.md @@ -34,35 +34,38 @@ class PriceColumn extends Column } /** - * Populate column callback. + * Position a column before/after another. * - * @return void + * @return array */ - public function populate( int $post_id ): void + public function position(): array { - echo '$' . get_post_meta( $post_id, '_price', true ); + return $this->after( 'title' ); } /** - * Set the column can be sorted. + * Populate column callback. * - * @return boolean + * @return callable */ - public function isSortable(): bool + public function populate(): callable { - return true; + return function( int $post_id ) { + echo '$' . get_post_meta( $post_id, '_price', true ); + }; } /** * Handle sorting the column by modifying the admin query. * - * @param $query \WP_Query - * @return void + * @return callable */ - public function sort(\WP_Query $query): void + public function sort(): callable { - $query->set( 'meta_key', '_price' ); - $query->set( 'orderby', 'meta_value_num' ); + return function( \WP_Query $query ) { + $query->set( 'meta_key', '_price' ); + $query->set( 'orderby', 'meta_value_num' ); + }; } } ``` diff --git a/docs/post-types/Modifying-columns.md b/docs/post-types/Modifying-columns.md index 362e300..0f10f3d 100644 --- a/docs/post-types/Modifying-columns.md +++ b/docs/post-types/Modifying-columns.md @@ -4,7 +4,7 @@ To modify a post types admin columns use the `column()` method. This method acce ## Adding Columns -To add columns to the admin edit screen pass an array of column slugs and labels to the `add()` method. +To add a column to the admin edit screen pass a column ket and label to the `label()` method. ```php use PostTypes\PostType; @@ -22,7 +22,7 @@ class Books extends PostType public function columns( Columns $column ): Columns { // Add a new price column. - $columns->add( 'price', __( 'Price', 'my-text-domain' ) ); + $columns->label( 'price', __( 'Price', 'my-text-domain' ) ); // Populate the price column with post meta. $columns->populate( 'price', function( $post_id ) { @@ -125,9 +125,9 @@ class Books extends PostType } ``` -## Column Order +## Column Positions -To rearrange columns pass an array of column slugs and position to the `order()` method. Only olumns you want to reorder need to be set, not all columns. +To rearrange columns pass an array of column slugs and position to the `position()` method. Only olumns you want to reorder need to be set, not all columns. ```php @@ -145,11 +145,8 @@ class Books extends PostType */ public function columns( Columns $column ): Columns { - // Order the new Rating and Genre columns. - $columns->order( [ - 'rating' => 2, - 'genre' => 4, - ] ); + // Position the rating column after the title column. + $columns->position( 'rating', 'after', 'title' ); return $columns; } diff --git a/docs/taxonomies/Modifying-columns.md b/docs/taxonomies/Modifying-columns.md index 0a5d1a3..be6d72f 100644 --- a/docs/taxonomies/Modifying-columns.md +++ b/docs/taxonomies/Modifying-columns.md @@ -22,7 +22,7 @@ class Genres extends Taxonomy public function columns( Columns $column ): Columns { // Add a new Popularity column. - $columns->add( 'popularity', __( 'Popularity', 'my-text-domain' ) ); + $columns->label( 'popularity', __( 'Popularity', 'my-text-domain' ) ); // Populate the popularity column with term meta. $columns->populate( 'popularity', function( $term_id ) { @@ -147,10 +147,8 @@ class Genres extends Taxonomy */ public function columns( Columns $column ): Columns { - // Order the new Popularity column. - $columns->order( [ - 'popularity' => 2, - ] ); + // Position the new Popularity column. + $columns->position( 'popularity', 'after', 'title' ); return $columns; } diff --git a/examples/books.php b/examples/books.php index 4fb1851..13d2b9a 100644 --- a/examples/books.php +++ b/examples/books.php @@ -72,6 +72,8 @@ public function columns( Columns $columns ): Columns { $columns->label( 'rating', __( 'Rating', 'post-types' ) ); + $columns->position( 'rating', 'after', 'price' ); + $columns->populate( 'rating', function( $post_id ) { echo get_post_meta( $post_id, 'rating', true ); } ); From 538602530841e6867a029a3d8d78549a71fd043a Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 17 Dec 2025 17:45:19 +0000 Subject: [PATCH 31/41] update columns reference --- src/ColumnBuilder.php | 16 ++++++------ tests/ColumBuilderTest.php | 52 +++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/ColumnBuilder.php b/src/ColumnBuilder.php index be599b5..5dc823d 100644 --- a/src/ColumnBuilder.php +++ b/src/ColumnBuilder.php @@ -9,7 +9,7 @@ class ColumnBuilder * * @var Columns */ - protected $manager; + protected $columns; /** * Column key. @@ -21,12 +21,12 @@ class ColumnBuilder /** * Constructor. * - * @param Columns $manager + * @param Columns $columns * @param string $key */ - public function __construct(Columns $manager, string $key) + public function __construct(Columns $columns, string $key) { - $this->manager = $manager; + $this->columns = $columns; $this->key = $key; } @@ -38,7 +38,7 @@ public function __construct(Columns $manager, string $key) */ public function label(string $label): ColumnBuilder { - $this->manager->add($this->key, $label); + $this->columns->label($this->key, $label); return $this; } @@ -52,7 +52,7 @@ public function label(string $label): ColumnBuilder */ public function position(string $direction, string $reference): ColumnBuilder { - $this->manager->position($this->key, $direction, $reference); + $this->columns->position($this->key, $direction, $reference); return $this; } @@ -87,7 +87,7 @@ public function before(string $reference): ColumnBuilder */ public function populate(callable $callback): ColumnBuilder { - $this->manager->populate($this->key, $callback); + $this->columns->populate($this->key, $callback); return $this; } @@ -100,7 +100,7 @@ public function populate(callable $callback): ColumnBuilder */ public function sort(callable $callback): ColumnBuilder { - $this->manager->sort($this->key, $callback); + $this->columns->sort($this->key, $callback); return $this; } diff --git a/tests/ColumBuilderTest.php b/tests/ColumBuilderTest.php index e5b3558..daed557 100644 --- a/tests/ColumBuilderTest.php +++ b/tests/ColumBuilderTest.php @@ -8,28 +8,28 @@ class ColumnBuilderTest extends TestCase { public function test_label_sets_column_label() { - $manager = $this->createMock(Columns::class); + $columns = $this->createMock(Columns::class); - $manager->expects($this->once()) - ->method('add') + $columns->expects($this->once()) + ->method('label') ->with('price', 'Price Label'); - $builder = new ColumnBuilder($manager, 'price'); + $builder = new ColumnBuilder($columns, 'price'); $result = $builder->label('Price Label'); - $this->assertSame($builder, $result); // fluent + $this->assertSame($builder, $result); } public function test_position_sets_position_correctly() { - $manager = $this->createMock(Columns::class); + $columns = $this->createMock(Columns::class); - $manager->expects($this->once()) + $columns->expects($this->once()) ->method('position') ->with('price', 'after', 'title'); - $builder = new ColumnBuilder($manager, 'price'); + $builder = new ColumnBuilder($columns, 'price'); $result = $builder->position('after', 'title'); @@ -38,13 +38,13 @@ public function test_position_sets_position_correctly() public function test_after_sets_position_after_reference() { - $manager = $this->createMock(Columns::class); + $columns = $this->createMock(Columns::class); - $manager->expects($this->once()) + $columns->expects($this->once()) ->method('position') ->with('price', 'after', 'title'); - $builder = new ColumnBuilder($manager, 'price'); + $builder = new ColumnBuilder($columns, 'price'); $result = $builder->after('title'); @@ -53,13 +53,13 @@ public function test_after_sets_position_after_reference() public function test_before_sets_position_before_reference() { - $manager = $this->createMock(Columns::class); + $columns = $this->createMock(Columns::class); - $manager->expects($this->once()) + $columns->expects($this->once()) ->method('position') ->with('price', 'before', 'title'); - $builder = new ColumnBuilder($manager, 'price'); + $builder = new ColumnBuilder($columns, 'price'); $result = $builder->before('title'); @@ -70,13 +70,13 @@ public function test_populate_sets_populate_callback() { $callback = function () {}; - $manager = $this->createMock(Columns::class); + $columns = $this->createMock(Columns::class); - $manager->expects($this->once()) + $columns->expects($this->once()) ->method('populate') ->with('price', $callback); - $builder = new ColumnBuilder($manager, 'price'); + $builder = new ColumnBuilder($columns, 'price'); $result = $builder->populate($callback); @@ -87,13 +87,13 @@ public function test_sort_sets_sort_callback() { $callback = function () {}; - $manager = $this->createMock(Columns::class); + $columns = $this->createMock(Columns::class); - $manager->expects($this->once()) + $columns->expects($this->once()) ->method('sort') ->with('price', $callback); - $builder = new ColumnBuilder($manager, 'price'); + $builder = new ColumnBuilder($columns, 'price'); $result = $builder->sort($callback); @@ -102,14 +102,14 @@ public function test_sort_sets_sort_callback() public function test_builder_fluency_all_methods_chain() { - $manager = $this->createMock(Columns::class); + $columns = $this->createMock(Columns::class); - $manager->expects($this->once())->method('add')->with('price', 'Price'); - $manager->expects($this->once())->method('position')->with('price', 'after', 'title'); - $manager->expects($this->once())->method('populate'); - $manager->expects($this->once())->method('sort'); + $columns->expects($this->once())->method('label')->with('price', 'Price'); + $columns->expects($this->once())->method('position')->with('price', 'after', 'title'); + $columns->expects($this->once())->method('populate'); + $columns->expects($this->once())->method('sort'); - $builder = new ColumnBuilder($manager, 'price'); + $builder = new ColumnBuilder($columns, 'price'); $result = $builder ->label('Price') From 7a8dbdd3c3d589529968193c052e424a29f744cf Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 17 Dec 2025 19:42:20 +0000 Subject: [PATCH 32/41] fix documentation --- README.md | 12 ++++++------ docs/Getting-started.md | 14 +++++++------- docs/post-types/Creating-columns.md | 5 ++--- docs/post-types/Defining-an-icon.md | 2 +- docs/post-types/Defining-hooks.md | 4 ++-- docs/post-types/Modifying-columns.md | 22 ++++++++++++---------- docs/taxonomies/Defining-hooks.md | 6 +++--- docs/taxonomies/Modifying-columns.md | 26 ++++++++++++++------------ 8 files changed, 47 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b9443ba..194d2a7 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ class Book extends PostType { 'view_item' => __( 'View Book', 'text-domain' ), 'search_items' => __( 'Search Books', 'text-domain' ), 'not_found' => __( 'No Books found', 'text-domain' ), - 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain'), + 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain' ), 'parent_item_colon' => __( 'Parent Book', 'text-domain' ), ]; } @@ -98,7 +98,7 @@ class Book extends PostType { public function filters(): array { return [ 'genre', - 'category' + 'category', ]; } @@ -127,11 +127,11 @@ class Book extends PostType { Once the custom post type class is created it can be registered to WordPress by instantiating and call the register method. ```php -// Instantiate the Books PostType class. -$books = new Books; +// Instantiate the Book PostType class. +$book = new Book; -// Register the books PostType to WordPress. -$books->register(); +// Register the Book PostType to WordPress. +$book->register(); ``` ## Notes diff --git a/docs/Getting-started.md b/docs/Getting-started.md index 532b2f8..cd1b5bd 100644 --- a/docs/Getting-started.md +++ b/docs/Getting-started.md @@ -54,7 +54,7 @@ class Book extends PostType { 'view_item' => __( 'View Book', 'text-domain' ), 'search_items' => __( 'Search Books', 'text-domain' ), 'not_found' => __( 'No Books found', 'text-domain' ), - 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain'), + 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain' ), 'parent_item_colon' => __( 'Parent Book', 'text-domain' ), ]; } @@ -76,8 +76,8 @@ class Book extends PostType { */ public function taxonomies(): array { return [ - 'genre' - 'category', + 'genre', + 'category' ]; } @@ -123,9 +123,9 @@ class Book extends PostType { Once the custom post type class is created it can be registered to WordPress by instantiating and call the register method. ```php -// Instantiate the Books PostType class. -$books = new Books; +// Instantiate the Book PostType class. +$book = new Book; -// Register the books PostType to WordPress. -$books->register(); +// Register the Book PostType to WordPress. +$book->register(); ``` diff --git a/docs/post-types/Creating-columns.md b/docs/post-types/Creating-columns.md index 6561bba..2d95e0b 100644 --- a/docs/post-types/Creating-columns.md +++ b/docs/post-types/Creating-columns.md @@ -78,10 +78,9 @@ class Book extends PostType { //... - public function columns( $columns ): void + public function columns( Columns $columns ): Columns { - $columns->column( new PriceColumn ); - + $columns->add( new PriceColumn ); return $columns; } } diff --git a/docs/post-types/Defining-an-icon.md b/docs/post-types/Defining-an-icon.md index f309206..71463e9 100644 --- a/docs/post-types/Defining-an-icon.md +++ b/docs/post-types/Defining-an-icon.md @@ -14,7 +14,7 @@ class Books extends PostType /** * Returns the admin menu icon for the Books post type. * - * @return array + * @return string */ public function icon(): string { diff --git a/docs/post-types/Defining-hooks.md b/docs/post-types/Defining-hooks.md index 7f03b42..7b47b5e 100644 --- a/docs/post-types/Defining-hooks.md +++ b/docs/post-types/Defining-hooks.md @@ -15,9 +15,9 @@ class Books extends PostType /** * Adds additional hooks for the post type. * - * @return array + * @return void */ - public function hooks(): array + public function hooks(): void { add_action( 'save_post_book', [ $this, 'onSave' ], 10, 3 ); } diff --git a/docs/post-types/Modifying-columns.md b/docs/post-types/Modifying-columns.md index 362e300..2962ff4 100644 --- a/docs/post-types/Modifying-columns.md +++ b/docs/post-types/Modifying-columns.md @@ -19,7 +19,7 @@ class Books extends PostType * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { // Add a new price column. $columns->add( 'price', __( 'Price', 'my-text-domain' ) ); @@ -31,8 +31,8 @@ class Books extends PostType // Make the price column sortable. $columns->sortable( 'price', function( WP_Query $query ) { - $query->set( 'orderby', 'meta_value_num' ); $query->set( 'meta_key', 'price' ); + $query->set( 'orderby', 'meta_value_num' ); } ); return $columns; @@ -57,12 +57,11 @@ class Books extends PostType * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { $columns->populate( 'rating', function( $post_id ) { echo get_post_meta( $post_id, 'rating', true ) . '/10'; } ); - return $columns; } } @@ -85,14 +84,13 @@ class Books extends PostType * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { // Make the rating column sortable. $columns->sortable( 'rating', function( WP_Query $query ) { - $query->set( 'orderby', 'meta_value_num' ); $query->set( 'meta_key', 'rating' ); + $query->set( 'orderby', 'meta_value_num' ); } ); - return $columns; } } @@ -115,11 +113,10 @@ class Books extends PostType * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { // Hide the Author and Date columns $columns->hide( [ 'author', 'date' ] ); - return $columns; } } @@ -143,14 +140,19 @@ class Books extends PostType * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { +<<<<<<< Updated upstream // Order the new Rating and Genre columns. $columns->order( [ 'rating' => 2, 'genre' => 4, ] ); +======= + // Position the rating column after the title column. + $columns->position( 'rating', 'after', 'title' ); +>>>>>>> Stashed changes return $columns; } } diff --git a/docs/taxonomies/Defining-hooks.md b/docs/taxonomies/Defining-hooks.md index bdfe5ee..9a01ab1 100644 --- a/docs/taxonomies/Defining-hooks.md +++ b/docs/taxonomies/Defining-hooks.md @@ -12,11 +12,11 @@ class Genres extends Taxonomy //... /** - * Adds additional hooks for the post type. + * Adds additional hooks for the taxonomy. * - * @return array + * @return void */ - public function hooks(): array + public function hooks(): void { add_action( 'saved_term', [ $this, 'onSave' ], 10, 5 ); } diff --git a/docs/taxonomies/Modifying-columns.md b/docs/taxonomies/Modifying-columns.md index 0a5d1a3..f1f0e13 100644 --- a/docs/taxonomies/Modifying-columns.md +++ b/docs/taxonomies/Modifying-columns.md @@ -19,20 +19,20 @@ class Genres extends Taxonomy * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { // Add a new Popularity column. $columns->add( 'popularity', __( 'Popularity', 'my-text-domain' ) ); // Populate the popularity column with term meta. $columns->populate( 'popularity', function( $term_id ) { - echo '$' . get_term_meta( $term_id, '_popularity', true ); + echo get_term_meta( $term_id, '_popularity', true ); } ); // Make the popularity column sortable. $columns->sortable( 'popularity', function( WP_Term_Query $query ) { - $query->query_vars['orderby'] = 'meta_value_num'; $query->query_vars['meta_key'] = 'popularity'; + $query->query_vars['orderby'] = 'meta_value_num'; } ); return $columns; @@ -57,13 +57,12 @@ class Genres extends Taxonomy * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { $columns->populate( 'popularity', function( $term_id ) { - echo '$' . get_term_meta( $term_id, '_popularity', true ); + echo get_term_meta( $term_id, '_popularity', true ); } ); - return $columns; } } @@ -87,14 +86,13 @@ class Genres extends Taxonomy * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { // Make the popularity column sortable. $columns->sortable( 'popularity', function( WP_Term_Query $query ) { - $query->query_vars['orderby'] = 'meta_value_num'; $query->query_vars['meta_key'] = 'popularity'; + $query->query_vars['orderby'] = 'meta_value_num'; } ); - return $columns; } } @@ -117,11 +115,10 @@ class Genres extends Taxonomy * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { // Hide the Description column. $columns->hide( [ 'description' ] ); - return $columns; } } @@ -145,13 +142,18 @@ class Genres extends Taxonomy * * @return array */ - public function columns( Columns $column ): Columns + public function columns( Columns $columns ): Columns { +<<<<<<< Updated upstream // Order the new Popularity column. $columns->order( [ 'popularity' => 2, ] ); +======= + // Position the new Popularity column. + $columns->position( 'popularity', 'after', 'title' ); +>>>>>>> Stashed changes return $columns; } } From 75605607712f4927249ece8bb418384568a327ea Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Wed, 17 Dec 2025 19:42:30 +0000 Subject: [PATCH 33/41] add migration guide draft --- docs/migrating-from-v2-v3.md | 248 +++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 docs/migrating-from-v2-v3.md diff --git a/docs/migrating-from-v2-v3.md b/docs/migrating-from-v2-v3.md new file mode 100644 index 0000000..6b19b5d --- /dev/null +++ b/docs/migrating-from-v2-v3.md @@ -0,0 +1,248 @@ +# Migrating from PostTypes v2.x to v3.0 + +This guide highlights the key changes and migration steps for upgrading from PostTypes v2.x to v3.0. The v3.0 release introduces significant architectural improvements, stricter typing, and new extension points. Review and update your custom post types, taxonomies, and integrations as described below. + +v3.0 shifts PostTypes to a declarative, class-based architecture to improve readability, testability, and long-term extensibility. + +> **Important:** v3.0 is a breaking release. Existing v2.x post type and taxonomy definitions will not work without modification. + +--- + +## Major Changes + +### 1. **Abstract Base Classes & Contracts** +- `PostType` and `Taxonomy` are now **abstract classes** and implement new PSR-4 contracts in `src/Contracts/`. +- You must implement all required abstract methods (e.g., `name()`, `labels()`, `options()`, etc.) in your custom classes. +- The base classes no longer provide magic property population or dynamic label/option generation. + +#### Previous PostTypes API + +Previously, post types were instantiated and the object methods used to configure the post type programatically. + +```php +columns()->hide( [ 'date', 'author' ] ); + +// Set the Books menu icon. +$books->icon( 'dashicons-book-alt' ); + +// Register the post type to WordPress. +$books->register(); +``` +#### New PostType API + +PostType is an abstract class and methods are used to configure the post type declaratively. + +```php +remove( [ 'date', 'author' ] ); + + return $columns; + } + + public function icon(): string { + return 'dashicons-book-alt'; + } +} +``` + +Registration remains the same by instantiating class and calling the `register()` method. + +```php +// inside functions.php or plugin file. + +$books = new App\PostTypes\Books; +$books->register(); +``` + +--- + +### 2. **Options, Labels, and Taxonomies** +All configuration (labels, options, taxonomies, supports, filters, columns, icon) must be provided via explicit methods. Only `name()` is strictly required; all other methods are optional and return sensible defaults. + + +```php +class Books extends PostType { + public function name(): string { + return 'book'; + } + + public function slug(): string { + return 'books'; + } + + public function labels(): array { + return [ + 'name' => __( 'Book', 'post-types' ), + 'singular_name' => __( 'Book', 'post-types' ), + 'plural_name' => __( 'Book', 'post-types' ), + ]; + } + + public function options(): array { + return [ + 'public' => true, + ]; + } + + public function taxonomies(): array { + return [ 'genres' ]; + } + + public function supports(): array { + return [ 'title', 'editor' ]; + } + + public function filters(): array { + return [ 'genres' ]; + } + + public function columns( Columns $columns ): Columns { + $columns->remove( [ 'date', 'author' ] ); + + return $columns; + } + + public function icon(): string { + return 'dashicons-book-alt'; + } +} +``` + +--- + +### 3. **Columns API** +The columns system is now managed via the `Columns` class, passed as a parameter to the PostType `columns()` method. + + +```php +class Books extends PostType { + + //... + + public function columns( Columns $columns ): Columns { + + $columns->label( 'rating', __( 'Rating', 'text-domain' ) ); + + $columns->populate( 'rating', function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ); + + return $columns; + } +} +``` + +Some methods on the `Columns` have changed or been replaced. + +- `add` has been replaced with `label`. +- add()` and `modify()` now return a Column Builder instance for fluent column configuration. +- `order` has been removed and replaced with a `position` API. +- A new `column` method allows passing `Column` classes for creating complex columns. + +The low-level `Columns` API is still available to use. For simple changes, you can call methods directly on the `Columns` instance. For more complex or fluent definitions, use the Column Builder via `add()` or `modify()`. + +```php +class Books extends PostType { + + //... + + public function columns( Columns $columns ): Columns { + + // Column Builder usage. + $columns->add( 'rating' ) + ->after( 'title' ) + ->label(__( 'Rating', 'text-domain' ) ) + ->populate( function ( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ) ); + } ); + + return $columns; + } +} +``` + +## Migration Steps + +1. **Update all custom PostType and Taxonomy classes:** + - Extend the new abstract base classes. + - Implement required methods (at minimum `name()`). + - Move configuration into explicit methods (labels, options, supports, etc). +2. **Update columns logic:** + - Use the new `Columns` API in your `columns()` method. +3. **Update registration:** + - Continue to call `register()` on your custom classes. +4. **Test thoroughly:** + - Run your test suite and verify admin UI behavior. + +--- + +## Example v2 vs v3 + +**v2.x:** +```php +// Import PostTypes. +use PostTypes\PostType; + +// 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' ); +``` + +**v3.0:** +```php +class Book extends PostType { + public function name(): string { + return 'book'; + } + + public function taxonomies(): array { + return [ 'genre' ]; + } + + public function columns(Columns $columns): Columns { + $columns->remove( [ 'date', 'author' ] ); + + return $columns; + } + + public function icon(): string { + return 'dashicons-book-alt'; + } +} +``` + +--- + +## Additional Notes +- See the updated README and docs for more examples and details. +- Review the new `src/Contracts/` interfaces for extension points. +- If you encounter issues, check the test suite and consult the [documentation](https://posttypes.jjgrainger.co.uk) From 0be0c5f0aa3a28b89ddbe9702c8b8ec746c345d6 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Mon, 22 Dec 2025 17:27:05 +0000 Subject: [PATCH 34/41] rename files and tweaks --- README.md | 4 +-- docs/SUMMARY.md | 35 ++++++++++--------- docs/post-types/README.md | 20 +++++------ ...{Creating-columns.md => create-columns.md} | 31 +++++++--------- ...supports.md => define-feature-supports.md} | 0 ...{Defining-filters.md => define-filters.md} | 2 +- .../{Defining-hooks.md => define-hooks.md} | 2 +- .../{Defining-an-icon.md => define-icon.md} | 0 .../{Defining-labels.md => define-labels.md} | 2 +- ...{Defining-options.md => define-options.md} | 2 +- ...ing-taxonomies.md => define-taxonomies.md} | 2 +- ...Modifying-columns.md => modify-columns.md} | 21 +++++------ docs/taxonomies/README.md | 12 +++---- .../{Defining-hooks.md => define-hooks.md} | 2 +- .../{Defining-labels.md => define-labels.md} | 0 ...{Defining-options.md => define-options.md} | 0 ...ing-post-types.md => define-post-types.md} | 0 ...Modifying-columns.md => modify-columns.md} | 0 18 files changed, 62 insertions(+), 73 deletions(-) rename docs/post-types/{Creating-columns.md => create-columns.md} (76%) rename docs/post-types/{Defining-feature-supports.md => define-feature-supports.md} (100%) rename docs/post-types/{Defining-filters.md => define-filters.md} (96%) rename docs/post-types/{Defining-hooks.md => define-hooks.md} (97%) rename docs/post-types/{Defining-an-icon.md => define-icon.md} (100%) rename docs/post-types/{Defining-labels.md => define-labels.md} (98%) rename docs/post-types/{Defining-options.md => define-options.md} (97%) rename docs/post-types/{Defining-taxonomies.md => define-taxonomies.md} (97%) rename docs/post-types/{Modifying-columns.md => modify-columns.md} (88%) rename docs/taxonomies/{Defining-hooks.md => define-hooks.md} (98%) rename docs/taxonomies/{Defining-labels.md => define-labels.md} (100%) rename docs/taxonomies/{Defining-options.md => define-options.md} (100%) rename docs/taxonomies/{Defining-post-types.md => define-post-types.md} (100%) rename docs/taxonomies/{Modifying-columns.md => modify-columns.md} (100%) diff --git a/README.md b/README.md index 194d2a7..6d623f9 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ class Book extends PostType { 'title', 'editor', 'thumbnail', - 'custom-fields' + 'custom-fields', ]; } @@ -80,7 +80,7 @@ class Book extends PostType { */ public function taxonomies(): array { return [ - 'genre' + 'genre', 'category', ]; } diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index ae46bff..f27d146 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,24 +1,25 @@ # Table of Contents * [PostTypes v3.0](../README.md) -* [Getting Started](Getting-started.md) +* [Migrating from v2 to v3](migrating-from-v2-v3.md) +* [Getting Started](getting-started.md) * [PostTypes](post-types/README.md) - * [Create a Post Type](post-types/Create-a-post-type.md) - * [Defining Labels](post-types/Defining-labels.md) - * [Defining Options](post-types/Defining-options.md) - * [Defining taxonomies](post-types/Defining-taxonomies.md) - * [Defining feature supports](post-types/Defining-feature-supports.md) - * [Defining an icon](post-types/Defining-an-icon.md) - * [Defining filters](post-types/Defining-filters.md) - * [Modifying columns](post-types/Modifying-columns.md) - * [Creating columns](post-types/Creating-columns.md) - * [Defining hooks](post-types/Defining-hooks.md) + * [Create a Post Type](post-types/create-a-post-type.md) + * [Define Labels](post-types/define-labels.md) + * [Define Options](post-types/define-options.md) + * [Define taxonomies](post-types/define-taxonomies.md) + * [Define feature supports](post-types/define-feature-supports.md) + * [Define an icon](post-types/define-icon.md) + * [Define filters](post-types/define-filters.md) + * [Modify columns](post-types/modify-columns.md) + * [Create columns](post-types/create-columns.md) + * [Define hooks](post-types/define-hooks.md) * [Taxonomies](taxonomies/README.md) - * [Create a Taxonomy](taxonomies/Create-a-taxonomy.md) - * [Defining Labels](taxonomies/Defining-labels.md) - * [Defining Options](taxonomies/Defining-options.md) - * [Defining Post Types](taxonomies/Defining-post-types.md) - * [Modifying Columns](taxonomies/Modifying-columns.md) - * [Defining Hooks](taxonomies/Defining-hooks.md) + * [Create a Taxonomy](taxonomies/create-a-taxonomy.md) + * [Define Labels](taxonomies/define-labels.md) + * [Define Options](taxonomies/define-options.md) + * [Define Post Types](taxonomies/define-post-types.md) + * [Modify Columns](taxonomies/modify-columns.md) + * [Define Hooks](taxonomies/define-hooks.md) * [Contributing](../CONTRIBUTING.md) * [Changelog](../Changelog.md) diff --git a/docs/post-types/README.md b/docs/post-types/README.md index 1b9c6f4..118be5b 100644 --- a/docs/post-types/README.md +++ b/docs/post-types/README.md @@ -2,13 +2,13 @@ The following section contains information on creating and working with post types. -* [Create a Post Type](Create-a-post-type.md) -* [Defining Labels](Defining-labels.md) -* [Defining Options](Defining-options.md) -* [Defining taxonomies](Defining-taxonomies.md) -* [Defining feature supports](Defining-feature-supports.md) -* [Defining an icon](Defining-an-icon.md) -* [Defining filters](Defining-filters.md) -* [Modifying columns](Modifying-columns.md) -* [Creating columns](Creating-columns.md) -* [Defining hooks](Defining-hooks.md) +* [Create a Post Type](create-a-post-type.md) +* [Define Labels](define-labels.md) +* [Define Options](define-options.md) +* [Define taxonomies](define-taxonomies.md) +* [Define feature supports](define-feature-supports.md) +* [Define an icon](define-an-icon.md) +* [Define filters](define-filters.md) +* [Modify columns](modify-columns.md) +* [Create columns](create-columns.md) +* [Define hooks](define-hooks.md) diff --git a/docs/post-types/Creating-columns.md b/docs/post-types/create-columns.md similarity index 76% rename from docs/post-types/Creating-columns.md rename to docs/post-types/create-columns.md index 2d95e0b..466ef27 100644 --- a/docs/post-types/Creating-columns.md +++ b/docs/post-types/create-columns.md @@ -1,4 +1,4 @@ -# Custom Columns +# Create Columns The `Column` class allows developers to create reusable, self-contained columns for the post listing table in the WordPress admin. These custom columns can display post meta, taxonomy values, or any custom data related to the post. @@ -36,33 +36,26 @@ class PriceColumn extends Column /** * Populate column callback. * - * @return void + * @return callable */ - public function populate( int $post_id ): void + public function populate(): callable { - echo '$' . get_post_meta( $post_id, '_price', true ); - } - - /** - * Set the column can be sorted. - * - * @return boolean - */ - public function isSortable(): bool - { - return true; + return function( int $post_id ) { + echo '$' . get_post_meta( $post_id, '_price', true ); + } } /** * Handle sorting the column by modifying the admin query. * - * @param $query \WP_Query - * @return void + * @return callable */ - public function sort(\WP_Query $query): void + public function sort(): callable { - $query->set( 'meta_key', '_price' ); - $query->set( 'orderby', 'meta_value_num' ); + return function( \WP_Query $query ) { + $query->set( 'meta_key', '_price' ); + $query->set( 'orderby', 'meta_value_num' ); + }; } } ``` diff --git a/docs/post-types/Defining-feature-supports.md b/docs/post-types/define-feature-supports.md similarity index 100% rename from docs/post-types/Defining-feature-supports.md rename to docs/post-types/define-feature-supports.md diff --git a/docs/post-types/Defining-filters.md b/docs/post-types/define-filters.md similarity index 96% rename from docs/post-types/Defining-filters.md rename to docs/post-types/define-filters.md index 0db583d..84c9b6d 100644 --- a/docs/post-types/Defining-filters.md +++ b/docs/post-types/define-filters.md @@ -1,4 +1,4 @@ -# Defining filters +# Define filters Filters that appear for the post type listing admin screen can be defined using the `filters()` method. diff --git a/docs/post-types/Defining-hooks.md b/docs/post-types/define-hooks.md similarity index 97% rename from docs/post-types/Defining-hooks.md rename to docs/post-types/define-hooks.md index 7b47b5e..e287c75 100644 --- a/docs/post-types/Defining-hooks.md +++ b/docs/post-types/define-hooks.md @@ -1,4 +1,4 @@ -# Defining hooks +# Define hooks Additional hooks are supported with the `hooks()` method. diff --git a/docs/post-types/Defining-an-icon.md b/docs/post-types/define-icon.md similarity index 100% rename from docs/post-types/Defining-an-icon.md rename to docs/post-types/define-icon.md diff --git a/docs/post-types/Defining-labels.md b/docs/post-types/define-labels.md similarity index 98% rename from docs/post-types/Defining-labels.md rename to docs/post-types/define-labels.md index 548135f..f36b866 100644 --- a/docs/post-types/Defining-labels.md +++ b/docs/post-types/define-labels.md @@ -1,4 +1,4 @@ -# Defining labels +# Define labels Labels for a PostType are defined in the `labels()` method and should return an array of labels. diff --git a/docs/post-types/Defining-options.md b/docs/post-types/define-options.md similarity index 97% rename from docs/post-types/Defining-options.md rename to docs/post-types/define-options.md index ab9a2ba..32bf2eb 100644 --- a/docs/post-types/Defining-options.md +++ b/docs/post-types/define-options.md @@ -1,4 +1,4 @@ -# Defining options +# Define options Options for a PostType are defined in the `options()` method and should return an array of valid [WordPress post type options](https://developer.wordpress.org/reference/functions/register_post_type/#parameters). diff --git a/docs/post-types/Defining-taxonomies.md b/docs/post-types/define-taxonomies.md similarity index 97% rename from docs/post-types/Defining-taxonomies.md rename to docs/post-types/define-taxonomies.md index fcf8da2..f4fd5f4 100644 --- a/docs/post-types/Defining-taxonomies.md +++ b/docs/post-types/define-taxonomies.md @@ -1,4 +1,4 @@ -# Defining Taxonomies +# Define Taxonomies Taxonomies for a PostType can be definied using the `taxonomies()` method. This method should return an array of taxonomy slugs to associate with the post type. diff --git a/docs/post-types/Modifying-columns.md b/docs/post-types/modify-columns.md similarity index 88% rename from docs/post-types/Modifying-columns.md rename to docs/post-types/modify-columns.md index 2962ff4..37353da 100644 --- a/docs/post-types/Modifying-columns.md +++ b/docs/post-types/modify-columns.md @@ -1,8 +1,8 @@ -# Modifying columns +# Modify columns To modify a post types admin columns use the `column()` method. This method accepts the `PostTypes\Columns` manager which has a variety of methods to help fine tune admin table columns. -## Adding Columns +## Add Columns To add columns to the admin edit screen pass an array of column slugs and labels to the `add()` method. @@ -62,6 +62,7 @@ class Books extends PostType $columns->populate( 'rating', function( $post_id ) { echo get_post_meta( $post_id, 'rating', true ) . '/10'; } ); + return $columns; } } @@ -91,6 +92,7 @@ class Books extends PostType $query->set( 'meta_key', 'rating' ); $query->set( 'orderby', 'meta_value_num' ); } ); + return $columns; } } @@ -117,14 +119,15 @@ class Books extends PostType { // Hide the Author and Date columns $columns->hide( [ 'author', 'date' ] ); + return $columns; } } ``` -## Column Order +## Position Columns -To rearrange columns pass an array of column slugs and position to the `order()` method. Only olumns you want to reorder need to be set, not all columns. +To rearrange columns use the `position` method to set a columns position before or after another. ```php @@ -142,17 +145,9 @@ class Books extends PostType */ public function columns( Columns $columns ): Columns { -<<<<<<< Updated upstream - // Order the new Rating and Genre columns. - $columns->order( [ - 'rating' => 2, - 'genre' => 4, - ] ); - -======= // Position the rating column after the title column. $columns->position( 'rating', 'after', 'title' ); ->>>>>>> Stashed changes + return $columns; } } diff --git a/docs/taxonomies/README.md b/docs/taxonomies/README.md index 042032a..570eafd 100644 --- a/docs/taxonomies/README.md +++ b/docs/taxonomies/README.md @@ -2,9 +2,9 @@ The following section contains information on creating and working with taxonomies. -* [Create a Taxonomy](Create-a-taxonomy.md) -* [Defining Labels](Defining-labels.md) -* [Defining Options](Defining-options.md) -* [Defining Post Types](Defining-post-types.md) -* [Modifying Columns](Modifying-columns.md) -* [Defining Hooks](Defining-hooks.md) +* [Create a Taxonomy](create-a-taxonomy.md) +* [Defune Labels](define-labels.md) +* [Defune Options](define-options.md) +* [Defune Post Types](define-post-types.md) +* [Modify Columns](modify-columns.md) +* [Defune Hooks](define-hooks.md) diff --git a/docs/taxonomies/Defining-hooks.md b/docs/taxonomies/define-hooks.md similarity index 98% rename from docs/taxonomies/Defining-hooks.md rename to docs/taxonomies/define-hooks.md index 9a01ab1..c13b4c6 100644 --- a/docs/taxonomies/Defining-hooks.md +++ b/docs/taxonomies/define-hooks.md @@ -1,4 +1,4 @@ -# Defining hooks +# Define hooks Additional hooks are supported with the `hooks()` method. diff --git a/docs/taxonomies/Defining-labels.md b/docs/taxonomies/define-labels.md similarity index 100% rename from docs/taxonomies/Defining-labels.md rename to docs/taxonomies/define-labels.md diff --git a/docs/taxonomies/Defining-options.md b/docs/taxonomies/define-options.md similarity index 100% rename from docs/taxonomies/Defining-options.md rename to docs/taxonomies/define-options.md diff --git a/docs/taxonomies/Defining-post-types.md b/docs/taxonomies/define-post-types.md similarity index 100% rename from docs/taxonomies/Defining-post-types.md rename to docs/taxonomies/define-post-types.md diff --git a/docs/taxonomies/Modifying-columns.md b/docs/taxonomies/modify-columns.md similarity index 100% rename from docs/taxonomies/Modifying-columns.md rename to docs/taxonomies/modify-columns.md From 98864efd2870d1ece63863e7db3526a9c797d9c4 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Mon, 22 Dec 2025 17:37:57 +0000 Subject: [PATCH 35/41] update migration guide --- docs/migrating-from-v2-v3.md | 64 ++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/docs/migrating-from-v2-v3.md b/docs/migrating-from-v2-v3.md index 6b19b5d..54dc29a 100644 --- a/docs/migrating-from-v2-v3.md +++ b/docs/migrating-from-v2-v3.md @@ -1,27 +1,26 @@ # Migrating from PostTypes v2.x to v3.0 -This guide highlights the key changes and migration steps for upgrading from PostTypes v2.x to v3.0. The v3.0 release introduces significant architectural improvements, stricter typing, and new extension points. Review and update your custom post types, taxonomies, and integrations as described below. +This guide highlights the key changes and migration steps for upgrading from PostTypes v2 to v3. The v3 release introduces significant changes. Review and update your custom post types, taxonomies, and integrations as described below. v3.0 shifts PostTypes to a declarative, class-based architecture to improve readability, testability, and long-term extensibility. -> **Important:** v3.0 is a breaking release. Existing v2.x post type and taxonomy definitions will not work without modification. +> **Important:** v3.0 is a breaking release. Existing v2 post type and taxonomy definitions will not work without modification. --- ## Major Changes ### 1. **Abstract Base Classes & Contracts** -- `PostType` and `Taxonomy` are now **abstract classes** and implement new PSR-4 contracts in `src/Contracts/`. -- You must implement all required abstract methods (e.g., `name()`, `labels()`, `options()`, etc.) in your custom classes. -- The base classes no longer provide magic property population or dynamic label/option generation. +- `PostType` and `Taxonomy` are now **abstract classes** and implement new contracts in `src/Contracts/`. +- You must implement the required `name()` abstract method in your custom classes. +- All other methods (e.g `labels()`, `options()`, `taxonomies()`, `columns()` etc.) must be used to pass the correct definitions for your post types and taxonomies. +- The base classes no longer provide magic property population or dynamic label/option generation. Post types and taxonomy properties must be explicitly defined. #### Previous PostTypes API Previously, post types were instantiated and the object methods used to configure the post type programatically. ```php -register(); PostType is an abstract class and methods are used to configure the post type declaratively. ```php -remove( [ 'date', 'author' ] ); return $columns; } + /** + * Set the post type menu icon. + */ public function icon(): string { return 'dashicons-book-alt'; } } ``` -Registration remains the same by instantiating class and calling the `register()` method. +Registration remains the same by instantiating class and calling the `register()` method inside your theme functions.php or plugin file. ```php // inside functions.php or plugin file. @@ -93,9 +100,8 @@ class Books extends PostType { public function labels(): array { return [ - 'name' => __( 'Book', 'post-types' ), + 'name' => __( 'Books', 'post-types' ), 'singular_name' => __( 'Book', 'post-types' ), - 'plural_name' => __( 'Book', 'post-types' ), ]; } @@ -156,7 +162,7 @@ class Books extends PostType { Some methods on the `Columns` have changed or been replaced. - `add` has been replaced with `label`. -- add()` and `modify()` now return a Column Builder instance for fluent column configuration. +- `add()` and `modify()` now return a Column Builder instance for fluent column configuration. - `order` has been removed and replaced with a `position` API. - A new `column` method allows passing `Column` classes for creating complex columns. @@ -203,6 +209,7 @@ class Books extends PostType { ```php // Import PostTypes. use PostTypes\PostType; +use PostTypes\Taxonomy; // Create a book post type. $books = new PostType( 'book' ); @@ -215,6 +222,20 @@ $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(); ``` **v3.0:** @@ -238,6 +259,23 @@ class Book extends PostType { return 'dashicons-book-alt'; } } + + +class Genre extends PostType { + public function name(): string { + return 'genre'; + } + + public function options(): array { + return [ + 'hierarchical' => false, + ]; + } +} + +(new Book)->register(); +(new Genre)->register(); + ``` --- From 6e5455393a948e5b1ce38e86866e757aa3584967 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Mon, 22 Dec 2025 18:16:31 +0000 Subject: [PATCH 36/41] update post type documentation --- docs/migrating-from-v2-v3.md | 6 +- docs/post-types/create-columns.md | 7 +- docs/post-types/define-feature-supports.md | 2 +- docs/post-types/define-filters.md | 4 +- docs/post-types/define-icon.md | 2 +- docs/post-types/define-labels.md | 2 +- docs/post-types/define-options.md | 4 +- docs/post-types/define-taxonomies.md | 2 +- docs/post-types/modify-columns.md | 151 +++++++++++++++++---- 9 files changed, 141 insertions(+), 39 deletions(-) diff --git a/docs/migrating-from-v2-v3.md b/docs/migrating-from-v2-v3.md index 54dc29a..bc8f8b9 100644 --- a/docs/migrating-from-v2-v3.md +++ b/docs/migrating-from-v2-v3.md @@ -240,6 +240,9 @@ $genres->register(); **v3.0:** ```php +use PostTypes\PostType; +use PostTypes\Taxonomy; + class Book extends PostType { public function name(): string { return 'book'; @@ -261,7 +264,7 @@ class Book extends PostType { } -class Genre extends PostType { +class Genre extends Taxonomy { public function name(): string { return 'genre'; } @@ -275,7 +278,6 @@ class Genre extends PostType { (new Book)->register(); (new Genre)->register(); - ``` --- diff --git a/docs/post-types/create-columns.md b/docs/post-types/create-columns.md index 466ef27..3f1440a 100644 --- a/docs/post-types/create-columns.md +++ b/docs/post-types/create-columns.md @@ -1,6 +1,6 @@ # Create Columns -The `Column` class allows developers to create reusable, self-contained columns for the post listing table in the WordPress admin. These custom columns can display post meta, taxonomy values, or any custom data related to the post. +The `Column` class allows developers to create reusable, self-contained columns for the post listing table in the WordPress admin. These custom columns can display post meta, taxonomy values, or any custom data related to the post or taxonomy. Columns are defined by extending the abstract `PostTypes\Column` class and implementing the required `name()` method, along with any optional logic such as rendering, sorting, or changing the label. @@ -62,7 +62,7 @@ class PriceColumn extends Column ## Adding the Column to a Post Type -Once you’ve defined your custom column, you can add it to a PostType using the `$columns->column()` method inside your `PostType` class: +Once you’ve defined your custom column, you can add it to a PostType using the `$columns->column()` method inside your `PostType` or `Taxonomy` class: ```php use PostTypes\PostType; @@ -73,7 +73,8 @@ class Book extends PostType public function columns( Columns $columns ): Columns { - $columns->add( new PriceColumn ); + $columns->column( new PriceColumn ); + return $columns; } } diff --git a/docs/post-types/define-feature-supports.md b/docs/post-types/define-feature-supports.md index 7608660..ea9a662 100644 --- a/docs/post-types/define-feature-supports.md +++ b/docs/post-types/define-feature-supports.md @@ -1,6 +1,6 @@ ## Define feature supports -You can define the features your post types supports using the `supports` method. This works similarly to the [`post_type_supports`](https://developer.wordpress.org/reference/functions/post_type_supports/) function in WordPress and returns an array of 'features'. +Features supported by your post types can be defined using the `supports` method. This works similarly to the [`post_type_supports`](https://developer.wordpress.org/reference/functions/post_type_supports/) function in WordPress and returns an array of 'features'. The `title` and `editor` features are enabled by default, matching the WordPress defaults. A list of available features can be seen in the [WordPress documentation](https://developer.wordpress.org/reference/functions/post_type_supports/#more-information). diff --git a/docs/post-types/define-filters.md b/docs/post-types/define-filters.md index 84c9b6d..3ffd179 100644 --- a/docs/post-types/define-filters.md +++ b/docs/post-types/define-filters.md @@ -2,9 +2,9 @@ Filters that appear for the post type listing admin screen can be defined using the `filters()` method. -This should return an array of taxonomy slugs that are to be used as dropdown filters for the post type. +This must return an array of taxonomy slugs that are to be used as dropdown filters for the post type. -An empty array is returned by default. +By default, an empty array is returned. ```php use PostTypes\PostType; diff --git a/docs/post-types/define-icon.md b/docs/post-types/define-icon.md index 71463e9..9ca43bb 100644 --- a/docs/post-types/define-icon.md +++ b/docs/post-types/define-icon.md @@ -1,6 +1,6 @@ # Menu Icons -WordPress has [Dashicons](https://developer.wordpress.org/resource/dashicons/), an icon font you can use with your custom post types. +[Dashicons](https://developer.wordpress.org/resource/dashicons/) is an icon font you can use with your post types. To set the post type icon pass the dashicon icon slug in the `icon()` method. diff --git a/docs/post-types/define-labels.md b/docs/post-types/define-labels.md index f36b866..b86e07e 100644 --- a/docs/post-types/define-labels.md +++ b/docs/post-types/define-labels.md @@ -1,6 +1,6 @@ # Define labels -Labels for a PostType are defined in the `labels()` method and should return an array of labels. +Labels for a post type are defined in the `labels()` method and should return an array of labels. By default, an empty array is returned and the WordPress default labels are used. diff --git a/docs/post-types/define-options.md b/docs/post-types/define-options.md index 32bf2eb..528ecdb 100644 --- a/docs/post-types/define-options.md +++ b/docs/post-types/define-options.md @@ -1,8 +1,8 @@ # Define options -Options for a PostType are defined in the `options()` method and should return an array of valid [WordPress post type options](https://developer.wordpress.org/reference/functions/register_post_type/#parameters). +Options for a post type are defined in the `options()` method and must return an array of valid [WordPress post type options](https://developer.wordpress.org/reference/functions/register_post_type/#parameters). -By default, an empty array is returned but these options are merged with a generated options array in [PostTypes](#) and whatever options are defined here will overwrite those defaults. +By default, an empty array is returned but these options are merged with a generated options array in PostTypes and whatever options are defined here will overwrite those defaults. See [`register_post_type()`](https://developer.wordpress.org/reference/functions/register_post_type/#parameters) for a full list of supported options. diff --git a/docs/post-types/define-taxonomies.md b/docs/post-types/define-taxonomies.md index f4fd5f4..8ec10c8 100644 --- a/docs/post-types/define-taxonomies.md +++ b/docs/post-types/define-taxonomies.md @@ -26,6 +26,6 @@ class Books extends PostType } ``` -This method only attaches the taxonomy to the post type, to _create_ a taxonomy see the [documentation](../taxonomies/Create-a-taxonomy.md) on creating a new taxonomy. +This method only attaches the taxonomy to the post type, to _create_ a taxonomy see the [documentation](../taxonomies/create-a-taxonomy.md) on creating a new taxonomy. Taxonomies and post types can be created and registered in any order. diff --git a/docs/post-types/modify-columns.md b/docs/post-types/modify-columns.md index 37353da..2826638 100644 --- a/docs/post-types/modify-columns.md +++ b/docs/post-types/modify-columns.md @@ -1,10 +1,10 @@ # Modify columns -To modify a post types admin columns use the `column()` method. This method accepts the `PostTypes\Columns` manager which has a variety of methods to help fine tune admin table columns. +To modify a post types admin columns use the `column()` method. This method accepts the `PostTypes\Columns` manager that has a variety of methods to help fine tune admin table columns. ## Add Columns -To add columns to the admin edit screen pass an array of column slugs and labels to the `add()` method. +Use the `add` method to create a column and initiate the fluent column builder API. The column builder provides useful methods for defining a number of column attributes. ```php use PostTypes\PostType; @@ -22,27 +22,85 @@ class Books extends PostType public function columns( Columns $columns ): Columns { // Add a new price column. - $columns->add( 'price', __( 'Price', 'my-text-domain' ) ); + $columns->add( 'price' ) + // Set the label. + ->label( __( 'Price', 'my-text-domain' ) ) + // Position the column after the title column. + ->after( 'title' ) + // Set the populate callback. + ->populate( function( $post_id ) { + echo '$' . get_post_meta( $post_id, '_price', true ); + } ) + // Set the sort callback. + ->sort( function( WP_Query $query ) { + $query->set( 'meta_key', 'price' ); + $query->set( 'orderby', 'meta_value_num' ); + } ); - // Populate the price column with post meta. - $columns->populate( 'price', function( $post_id ) { - echo '$' . get_post_meta( $post_id, '_price', true ); - } ); + return $columns; + } +} +``` - // Make the price column sortable. - $columns->sortable( 'price', function( WP_Query $query ) { - $query->set( 'meta_key', 'price' ); - $query->set( 'orderby', 'meta_value_num' ); - } ); +## Modify a column + +Any column can be modified using the `modify` method. + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $columns ): Columns + { + // Update the WordPress author column label. + $columns->modify( 'author' )->label( __( 'Post Author', 'my-text-domain' ) ); + + return $columns; + } +} +``` + +## Position Columns + +To rearrange columns use either the `before` or `after` methods to set a columns position before or after another. + + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $columns ): Columns + { + // Position the price column after the title column. + $columns->add( 'price' )->after( 'title' ); return $columns; } } ``` + ## Populate Columns -To populate any column use the `populate()` method, by passing the column slug and a callback function. +To populate a column use the `populate()` method passing a callback function. ```php use PostTypes\PostType; @@ -59,8 +117,8 @@ class Books extends PostType */ public function columns( Columns $columns ): Columns { - $columns->populate( 'rating', function( $post_id ) { - echo get_post_meta( $post_id, 'rating', true ) . '/10'; + $columns->add( 'price' )->populate( function( $post_id ) { + echo '$' . get_post_meta( $post_id, '_price', true ); } ); return $columns; @@ -70,7 +128,7 @@ class Books extends PostType ## Sortable Columns -To define which custom columns are sortable use the `sortable()` method. +To make a column sortable use the `sort()` method and pass the sorting callback. ```php use PostTypes\PostType; @@ -88,8 +146,8 @@ class Books extends PostType public function columns( Columns $columns ): Columns { // Make the rating column sortable. - $columns->sortable( 'rating', function( WP_Query $query ) { - $query->set( 'meta_key', 'rating' ); + $columns->add( 'price' )->sort( function( WP_Query $query ) { + $query->set( 'meta_key', 'price' ); $query->set( 'orderby', 'meta_value_num' ); } ); @@ -98,9 +156,9 @@ class Books extends PostType } ``` -## Hide Columns +## Remove Columns -To hide columns pass the column slug to the `hide()` method. For multiple columns pass an array of column slugs. +To remove columns pass the column slug to the `remove()` method. For multiple columns pass an array of column slugs. ```php use PostTypes\PostType; @@ -118,17 +176,16 @@ class Books extends PostType public function columns( Columns $columns ): Columns { // Hide the Author and Date columns - $columns->hide( [ 'author', 'date' ] ); + $columns->remove( [ 'author', 'date' ] ); return $columns; } } ``` -## Position Columns - -To rearrange columns use the `position` method to set a columns position before or after another. +## Whitelist Columns +Use the `only()` method to define what columns should appear by passing an array of column slugs. ```php use PostTypes\PostType; @@ -145,12 +202,54 @@ class Books extends PostType */ public function columns( Columns $columns ): Columns { - // Position the rating column after the title column. - $columns->position( 'rating', 'after', 'title' ); + // Only show the checkbox, title and price columns. + $columns->only( [ 'cb', 'title', 'price' ] ); return $columns; } } ``` +## Low-level API + +The Columns class has a low-level API that can continue to be used to make and modify columns, however it is recommended to use the column builder API shown above. + +Below is an example of how to use the low-level API to create the price column. + +```php +use PostTypes\PostType; +use PostTypes\Columns; + +class Books extends PostType +{ + //... + + /** + * Set the PostTypes admin columns. + * + * @return array + */ + public function columns( Columns $columns ): Columns + { + // Add a new price column. + $columns->label( 'price', __( 'Price', 'my-text-domain' ) ); + + // Position the column after the title column. + $columns->position( 'price', 'after', 'title' ); + + // Set the populate callback. + $columns->populate( 'price', function( $post_id ) { + echo '$' . get_post_meta( $post_id, '_price', true ); + } ); + + // Set the sort callback. + $columns->sort( 'price', function( WP_Query $query ) { + $query->set( 'meta_key', 'price' ); + $query->set( 'orderby', 'meta_value_num' ); + } ); + + return $columns; + } +} +``` From fd8aee698e0fa384f525b97f3072225c8e4781b0 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Mon, 29 Dec 2025 18:02:08 +0000 Subject: [PATCH 37/41] update README and file names --- README.md | 8 +- docs/getting-started.md | 131 ++++++++++++++++++++++++++ docs/post-types/Create-a-post-type.md | 2 +- docs/post-types/create-a-post-type.md | 36 +++++++ docs/post-types/define-labels.md | 2 +- docs/taxonomies/create-a-taxonomy.md | 63 +++++++++++++ examples/Books.php | 99 +++++++++++++++++++ 7 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 docs/getting-started.md create mode 100644 docs/post-types/create-a-post-type.md create mode 100644 docs/taxonomies/create-a-taxonomy.md create mode 100644 examples/Books.php diff --git a/README.md b/README.md index 6d623f9..d026efc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -# PostTypes v2.2.1 +# PostTypes v3.0 [![tests](https://github.com/jjgrainger/PostTypes/actions/workflows/tests.yml/badge.svg)](https://github.com/jjgrainger/PostTypes/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/jjgrainger/PostTypes/branch/master/graph/badge.svg?token=SGrK2xDF46)](https://codecov.io/gh/jjgrainger/PostTypes) [![Latest Stable Version](https://flat.badgen.net/github/release/jjgrainger/PostTypes/stable)](https://packagist.org/packages/jjgrainger/posttypes) [![Total Downloads](https://flat.badgen.net/packagist/dt/jjgrainger/PostTypes)](https://packagist.org/packages/jjgrainger/posttypes) [![License](https://flat.badgen.net/github/license/jjgrainger/PostTypes)](https://packagist.org/packages/jjgrainger/posttypes) -> Simple WordPress custom post types. +> Modern PHP abstractions for WordPress post types and taxonomies. + +## Migrating from v2 to v3 + +> Important: v3.0 is a breaking release. Existing v2 post type and taxonomy definitions will not work without modification. ## Requirements diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..cd1b5bd --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,131 @@ +# Getting Started + +## Requirements + +* PHP >=8.1 +* [Composer](https://getcomposer.org/) +* [WordPress](https://wordpress.org) >=6.3 + +## Installation + +#### Install with composer + +Run the following in your terminal to install PostTypes with [Composer](https://getcomposer.org/). + +``` +$ composer require jjgrainger/posttypes +``` + +PostTypes uses [PSR-4](https://www.php-fig.org/psr/psr-4/) autoloading and can be used with the Composer's autoloader. 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 + +#### Create a custom post type + +Custom post types are defined as classes that extend the base `PostType` class. At a minimum, the `name` method must be implemented to define the post type slug. All other methods are optional and allow you to configure labels, options, taxonomies, admin columns, filters, and more as needed. + +```php + __( 'Book', 'text-domain' ), + 'singular_name' => __( 'Book', 'text-domain' ), + 'menu_name' => __( 'Books', 'text-domain' ), + 'all_items' => __( 'Books', 'text-domain' ), + 'add_new' => __( 'Add New', 'text-domain' ), + 'add_new_item' => __( 'Add New Book', 'text-domain' ), + 'edit_item' => __( 'Edit Book', 'text-domain' ), + 'new_item' => __( 'New Book', 'text-domain' ), + 'view_item' => __( 'View Book', 'text-domain' ), + 'search_items' => __( 'Search Books', 'text-domain' ), + 'not_found' => __( 'No Books found', 'text-domain' ), + 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain' ), + 'parent_item_colon' => __( 'Parent Book', 'text-domain' ), + ]; + } + + /** + * Define Post Type feature supports. + */ + public function supports(): array { + return [ + 'title', + 'editor', + 'thumbnail', + 'custom-fields' + ]; + } + + /** + * Define Taxonomies associated with the Post Type. + */ + public function taxonomies(): array { + return [ + 'genre', + 'category' + ]; + } + + /** + * Set the menu icon for the Post Type. + */ + public function icon(): string { + return 'dashicons-book'; + } + + /** + * Set the admin post table filters. + */ + public function filters(): array { + return [ + 'genre', + 'category' + ]; + } + + /** + * Define the columns for the admin post table. + */ + public function columns(Columns $columns): Columns { + // Remove the author and date column. + $columns->remove( [ 'author', 'date' ] ); + + // Add a Rating column. + $columns->add( 'rating', __( 'Rating', 'post-types' ) ); + + // Populate the rating column. + $columns->populate( 'rating', function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ); + + return $columns; + } +} +``` + +### Register a custom post type + +Once the custom post type class is created it can be registered to WordPress by instantiating and call the register method. + +```php +// Instantiate the Book PostType class. +$book = new Book; + +// Register the Book PostType to WordPress. +$book->register(); +``` diff --git a/docs/post-types/Create-a-post-type.md b/docs/post-types/Create-a-post-type.md index d843efe..183b014 100644 --- a/docs/post-types/Create-a-post-type.md +++ b/docs/post-types/Create-a-post-type.md @@ -21,7 +21,7 @@ class Books extends PostType ## Register PostType to WordPress -Once your PostType class is created the new post type can be registered to WordPress by instantiating the class and calling the `register()` method in your plugin or theme. +Once your PostType class is created it can be registered to WordPress by instantiating the class and calling the `register()` method in your plugin or theme. ```php // Instantiate the Books PostType class. diff --git a/docs/post-types/create-a-post-type.md b/docs/post-types/create-a-post-type.md new file mode 100644 index 0000000..183b014 --- /dev/null +++ b/docs/post-types/create-a-post-type.md @@ -0,0 +1,36 @@ +# Create a Post Type + +Post types can be made by creating a new class that extends the `PostType` abstract class. All PostType classes require you to implement the `name()` method. Below is an example of a simple Books PostType class to get started. + +```php +use PostTypes\PostType; + +class Books extends PostType +{ + /** + * Returns the post type name to register to WordPress. + * + * @return string + */ + public function name(): string + { + return 'book'; + } +} +``` + +## Register PostType to WordPress + +Once your PostType class is created it can be registered to WordPress by instantiating the class and calling the `register()` method in your plugin or theme. + +```php +// Instantiate the Books PostType class. +$books = new Books; + +// Register the books PostType to WordPress. +$books->register(); +``` + +{% hint style="info" %} +The `register()` method hooks into WordPress and sets all the actions and filters required to create your custom post type. You do not need to add any of your PostTypes code in actions/filters. Doing so may lead to unexpected results. +{% endhint %} diff --git a/docs/post-types/define-labels.md b/docs/post-types/define-labels.md index b86e07e..7b8f911 100644 --- a/docs/post-types/define-labels.md +++ b/docs/post-types/define-labels.md @@ -1,6 +1,6 @@ # Define labels -Labels for a post type are defined in the `labels()` method and should return an array of labels. +Labels for a post type are defined in the `labels()` method and must return an array of labels. By default, an empty array is returned and the WordPress default labels are used. diff --git a/docs/taxonomies/create-a-taxonomy.md b/docs/taxonomies/create-a-taxonomy.md new file mode 100644 index 0000000..107703a --- /dev/null +++ b/docs/taxonomies/create-a-taxonomy.md @@ -0,0 +1,63 @@ +# Taxonomies + +Taxonomies are created using the `Taxonomy` class. This works identically to the `PostType` class and holds similar methods. + +## Create a new taxonomy + +To create a new taxonomy pass the taxonomy name to the class constructor. Labels and the slug are generated from the taxonomy name. + +```php +use PostTypes\Taxonomy; + +class Genres extends Taxonomy +{ + /** + * Returns the taxonomy name to register to WordPress. + * + * @return string + */ + public function name(): string + { + return 'genre'; + } +} +``` + +## Set the slug for the Taxonomy + +By default, the Taxonomy name is used as the slug for the taxonomy too. To change this use the `slug()` method to return a slug string. + +```php +use PostTypes\Taxonomy; + +class Genres extends Taxonomy +{ + //... + + /** + * Returns the taxonomy slug. + * + * @return string + */ + public function slug(): string + { + return 'genres'; + } +} +``` + +## Register the Taxonomy to WordPress + +Once your Taxonomy class is created it can be registered to WordPress by instantiating the class and calling the `register()` method in your plugin or theme. + +```php +// Instantiate the Genres Taxonomy class. +$genres = new Genres; + +// Register the Genres Taxonomy to WordPress. +$genres->register(); +``` + +{% hint style="info" %} +The `register()` method hooks into WordPress and sets all the actions and filters required to create your taxonomy. You do not need to add any of your Taxonomy code in actions/filters. Doing so may lead to unexpected results. +{% endhint %} diff --git a/examples/Books.php b/examples/Books.php new file mode 100644 index 0000000..13d2b9a --- /dev/null +++ b/examples/Books.php @@ -0,0 +1,99 @@ + __( 'Book', 'post-types' ), + 'singular_name' => __( 'Book', 'post-types' ), + 'menu_name' => __( 'Books', 'post-types' ), + 'all_items' => __( 'Books', 'post-types' ), + 'add_new' => __( 'Add New', 'post-types' ), + 'add_new_item' => __( 'Add New Book', 'post-types' ), + 'edit_item' => __( 'Edit Book', 'post-types' ), + 'new_item' => __( 'New Book', 'post-types' ), + 'view_item' => __( 'View Book', 'post-types' ), + 'search_items' => __( 'Search Books', 'post-types' ), + 'not_found' => __( 'No Books found', 'post-types' ), + 'not_found_in_trash' => __( 'No Books found in Trash', 'post-types'), + 'parent_item_colon' => __( 'Parent Book', 'post-types' ), + ]; + } + + public function taxonomies(): array { + return [ + 'post_tag', + 'genre', + ]; + } + + public function supports(): array { + return [ + 'title', + 'editor', + 'author', + 'custom-fields', + ]; + } + + public function options(): array { + return [ + 'show_in_rest' => false, + ]; + } + + public function icon(): string { + return 'dashicons-book'; + } + + public function filters(): array { + return [ + 'genre', + 'post_tag', + ]; + } + + public function columns( Columns $columns ): Columns { + + $columns->remove( [ 'author', 'date' ] ); + + $columns->column( new Price ); + + $columns->label( 'rating', __( 'Rating', 'post-types' ) ); + + $columns->position( 'rating', 'after', 'price' ); + + $columns->populate( 'rating', function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ); + + $columns->sort( 'rating', function( $query ) { + $query->set('orderby', 'meta_value_num'); + $query->set('meta_key', 'rating'); + } ); + + $columns->add( 'rating' ) + ->after( 'price' ) + ->label( __( 'Rating', 'post-types' ) ) + ->populate( function( $post_id ) { + echo get_post_meta( $post_id, 'rating', true ); + } ) + ->sort( function( $query ) { + $query->set('orderby', 'meta_value_num'); + $query->set('meta_key', 'rating'); + } ); + + return $columns; + } +} From 3d672369d1738c77413d1127d48917adc2401ca0 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Mon, 29 Dec 2025 18:05:19 +0000 Subject: [PATCH 38/41] add migration guide to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d026efc..07aba50 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Migrating from v2 to v3 -> Important: v3.0 is a breaking release. Existing v2 post type and taxonomy definitions will not work without modification. +> **Important**: v3.0 is a breaking release. Existing v2 post type and taxonomy definitions will not work without modification. Please review the migration guide in the [documentation](https://posttypes.jjgrainger.co.uk) on how to upgrade to version 3. ## Requirements From cf1d32d18c1e40f06b215ff8e461fc81447b50c6 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Mon, 29 Dec 2025 18:23:25 +0000 Subject: [PATCH 39/41] update Taxonomy documentation --- docs/taxonomies/Create-a-taxonomy.md | 2 +- docs/taxonomies/README.md | 9 +-- docs/taxonomies/create-a-taxonomy.md | 2 +- docs/taxonomies/create-columns.md | 91 ++++++++++++++++++++++++++++ docs/taxonomies/define-hooks.md | 2 +- docs/taxonomies/define-labels.md | 16 ++--- docs/taxonomies/define-options.md | 4 +- docs/taxonomies/define-post-types.md | 6 +- docs/taxonomies/modify-columns.md | 49 +++++++-------- 9 files changed, 137 insertions(+), 44 deletions(-) create mode 100644 docs/taxonomies/create-columns.md diff --git a/docs/taxonomies/Create-a-taxonomy.md b/docs/taxonomies/Create-a-taxonomy.md index 107703a..7507660 100644 --- a/docs/taxonomies/Create-a-taxonomy.md +++ b/docs/taxonomies/Create-a-taxonomy.md @@ -4,7 +4,7 @@ Taxonomies are created using the `Taxonomy` class. This works identically to the ## Create a new taxonomy -To create a new taxonomy pass the taxonomy name to the class constructor. Labels and the slug are generated from the taxonomy name. +Taxonomies are made by creating a new class that extends the `Taxonomy` abstract class. All Taxonomy classes require you to implement the `name()` method. ```php use PostTypes\Taxonomy; diff --git a/docs/taxonomies/README.md b/docs/taxonomies/README.md index 570eafd..af91b69 100644 --- a/docs/taxonomies/README.md +++ b/docs/taxonomies/README.md @@ -3,8 +3,9 @@ The following section contains information on creating and working with taxonomies. * [Create a Taxonomy](create-a-taxonomy.md) -* [Defune Labels](define-labels.md) -* [Defune Options](define-options.md) -* [Defune Post Types](define-post-types.md) +* [Define Labels](define-labels.md) +* [Define Options](define-options.md) +* [Define Post Types](define-post-types.md) * [Modify Columns](modify-columns.md) -* [Defune Hooks](define-hooks.md) +* [Create Columns](create-columns.md) +* [Define Hooks](define-hooks.md) diff --git a/docs/taxonomies/create-a-taxonomy.md b/docs/taxonomies/create-a-taxonomy.md index 107703a..7507660 100644 --- a/docs/taxonomies/create-a-taxonomy.md +++ b/docs/taxonomies/create-a-taxonomy.md @@ -4,7 +4,7 @@ Taxonomies are created using the `Taxonomy` class. This works identically to the ## Create a new taxonomy -To create a new taxonomy pass the taxonomy name to the class constructor. Labels and the slug are generated from the taxonomy name. +Taxonomies are made by creating a new class that extends the `Taxonomy` abstract class. All Taxonomy classes require you to implement the `name()` method. ```php use PostTypes\Taxonomy; diff --git a/docs/taxonomies/create-columns.md b/docs/taxonomies/create-columns.md new file mode 100644 index 0000000..552ebd8 --- /dev/null +++ b/docs/taxonomies/create-columns.md @@ -0,0 +1,91 @@ +# Create Columns + +The `Column` class allows developers to create reusable, self-contained columns for the taxonomy and post listing table in the WordPress admin. These custom columns can display any custom data related to the taxonomy. + +Columns are defined by extending the abstract `PostTypes\Column` class and implementing the required `name()` method, along with any optional logic such as rendering, sorting, or changing the label. + +## Creating a Custom Column + +To create a custom column, extend the base `Column` class and implement the methods you need. Here's an example of a `PopularityColumn` that pulls a `_popularity` meta field from the term and displays it in the admin table: + +```php +use PostTypes\Column; + +class PopularityColumn extends Column +{ + /** + * Defines the column key used internally. + * + * @return string. + */ + public function name(): string + { + return 'populariy'; + } + + /** + * Define the column label. + * + * @return string + */ + public function label(): string + { + return __( 'Popularity', 'my-text-domain' ); + } + + /** + * Position a column before/after another. + * + * @return array + */ + public function position(): array + { + return $this->after( 'title' ); + } + + /** + * Populate column callback. + * + * @return callable + */ + public function populate(): callable + { + return function( int $term_id ) { + echo get_term_meta( $term_id, '_popularity', true ); + }; + } + + /** + * Handle sorting the column by modifying the admin query. + * + * @return callable + */ + public function sort(): callable + { + return function( \WP_Term_Query $query ) { + $query->query_vars['meta_key'] = '_popularity'; + $query->query_vars['orderby'] = 'meta_value_num'; + }; + } +} +``` + +## Adding the Column to a Taxonomy + +Once you’ve defined your custom column, you can add it to a PostType using the `$columns->column()` method inside your `Taxonomy` class: + +```php +use PostTypes\Taxonomy; + +class Genres extends Taxonomy +{ + //... + + public function columns( Columns $columns ): Columns + { + $columns->column( new PopularityColumn ); + + return $columns; + } +} +``` diff --git a/docs/taxonomies/define-hooks.md b/docs/taxonomies/define-hooks.md index c13b4c6..8d9028d 100644 --- a/docs/taxonomies/define-hooks.md +++ b/docs/taxonomies/define-hooks.md @@ -2,7 +2,7 @@ Additional hooks are supported with the `hooks()` method. -Here you can register additional actions and filters to WordPress and allows you to keep logic associated with your post type in one class. +Here you can register additional actions and filters to WordPress and allows you to keep logic associated with your taxonomy in one class. ```php use PostTypes\Taxonomy; diff --git a/docs/taxonomies/define-labels.md b/docs/taxonomies/define-labels.md index e0f3bbc..98ad264 100644 --- a/docs/taxonomies/define-labels.md +++ b/docs/taxonomies/define-labels.md @@ -1,6 +1,6 @@ -# Defining labels +# Define labels -Labels for a Taxonomy are defined in the `labels()` method and should return an array of labels. +Labels for a Taxonomy are defined in the `labels()` method and must return an array of labels. By default, an empty array is returned and the WordPress default labels are used. @@ -21,12 +21,12 @@ class Genres extends Taxonomy public function labels(): array { return [ - 'name' => __( 'Genres', 'my-text-domain' ), - 'singular_name' => __( 'Genre', 'my-text-domain' ), - 'search_items' => __( 'Search Genres', 'my-text-domain' ), - 'all_items' => __( 'Genres', 'my-text-domain' ), - 'edit_item' => __( 'Edit Genre', 'my-text-domain' ), - 'view_item' => __( 'View Genre', 'my-text-domain' ), + 'name' => __( 'Genres', 'my-text-domain' ), + 'singular_name' => __( 'Genre', 'my-text-domain' ), + 'search_items' => __( 'Search Genres', 'my-text-domain' ), + 'all_items' => __( 'Genres', 'my-text-domain' ), + 'edit_item' => __( 'Edit Genre', 'my-text-domain' ), + 'view_item' => __( 'View Genre', 'my-text-domain' ), ]; } } diff --git a/docs/taxonomies/define-options.md b/docs/taxonomies/define-options.md index f8f6c2e..0dceafc 100644 --- a/docs/taxonomies/define-options.md +++ b/docs/taxonomies/define-options.md @@ -1,6 +1,6 @@ -# Defining options +# Define options -Options for a Taxonomy are defined in the `options()` method and should return an array of valid [WordPress taxonomy options](https://developer.wordpress.org/reference/functions/register_taxonomy/#parameters). +Options for a Taxonomy are defined in the `options()` method and must return an array of valid [WordPress taxonomy options](https://developer.wordpress.org/reference/functions/register_taxonomy/#parameters). By default, an empty array is returned. diff --git a/docs/taxonomies/define-post-types.md b/docs/taxonomies/define-post-types.md index 95eef00..46ea59b 100644 --- a/docs/taxonomies/define-post-types.md +++ b/docs/taxonomies/define-post-types.md @@ -1,6 +1,6 @@ -# Defining Post Types +# Define Post Types -Post types for a Taxonomy can be definied using the `posttypes()` method. This method should return an array of post type names to associate with the taxonomy. +Post types can be added to a Taxonomy using the `posttypes()` method. This method should return an array of post type names to associate with the taxonomy. An empty array is returned by default and no post types are attached to the Taxonomy. @@ -26,6 +26,6 @@ class Genres extends Taxonomy } ``` -This method only attaches the post type to the taxonomy, to _create_ a post type see the [documentation](../post-types/Create-a-post-type.md) on creating a new post type. +This method only attaches the post type to the taxonomy, to _create_ a post type see the [documentation](../post-types/create-a-post-type.md) on creating a new post type. Taxonomies and post types can be created and registered in any order. diff --git a/docs/taxonomies/modify-columns.md b/docs/taxonomies/modify-columns.md index 981460f..909e9c8 100644 --- a/docs/taxonomies/modify-columns.md +++ b/docs/taxonomies/modify-columns.md @@ -1,10 +1,10 @@ -# Modifying columns +# Modify columns To modify a taxonomies admin columns use the `column()` method. This method accepts the `PostTypes\Columns` manager which has a variety of methods to help fine tune admin table columns. -## Adding Columns +## Add Columns -To add columns to the admin list table pass an array of column slugs and labels to the `add()` method. +Use the `add` method to create a column and initiate the fluent column builder API. The column builder provides useful methods for defining a number of column attributes. ```php use PostTypes\Taxonomy; @@ -17,23 +17,25 @@ class Genres extends Taxonomy /** * Set the Taxonomy admin columns. * - * @return array + * @return Columns */ public function columns( Columns $columns ): Columns { // Add a new Popularity column. - $columns->label( 'popularity', __( 'Popularity', 'my-text-domain' ) ); - - // Populate the popularity column with term meta. - $columns->populate( 'popularity', function( $term_id ) { - echo get_term_meta( $term_id, '_popularity', true ); - } ); - - // Make the popularity column sortable. - $columns->sortable( 'popularity', function( WP_Term_Query $query ) { - $query->query_vars['meta_key'] = 'popularity'; - $query->query_vars['orderby'] = 'meta_value_num'; - } ); + $columns->add( 'popularity' ) + // Set the label. + ->label( __( 'Popularity', 'my-text-domain' ) ); + // Position the column after the title column. + ->after( 'title' ) + // Populate the popularity column with term meta. + >populate( 'popularity', function( $term_id ) { + echo get_term_meta( $term_id, '_popularity', true ); + } ); + // Make the popularity column sortable. + ->sortable( 'popularity', function( WP_Term_Query $query ) { + $query->query_vars['meta_key'] = '_popularity'; + $query->query_vars['orderby'] = 'meta_value_num'; + } ); return $columns; } @@ -42,7 +44,7 @@ class Genres extends Taxonomy ## Populate Columns -To populate any column use the `populate()` method, by passing the column slug and a callback function. +To populate any column use the `populate()` method and passing a callback function. ```php use PostTypes\Taxonomy; @@ -59,7 +61,7 @@ class Genres extends Taxonomy */ public function columns( Columns $columns ): Columns { - $columns->populate( 'popularity', function( $term_id ) { + $columns->add( 'popularity' )->populate( function( $term_id ) { echo get_term_meta( $term_id, '_popularity', true ); } ); @@ -70,7 +72,7 @@ class Genres extends Taxonomy ## Sortable Columns -To define which custom columns are sortable use the `sortable()` method. +To define which columns are sortable use the `sort()` method. ```php use PostTypes\Taxonomy; @@ -89,10 +91,11 @@ class Genres extends Taxonomy public function columns( Columns $columns ): Columns { // Make the popularity column sortable. - $columns->sortable( 'popularity', function( WP_Term_Query $query ) { - $query->query_vars['meta_key'] = 'popularity'; + $columns->add( 'popularity' )->sort( function( WP_Term_Query $query ) { + $query->query_vars['meta_key'] = '_popularity'; $query->query_vars['orderby'] = 'meta_value_num'; } ); + return $columns; } } @@ -125,7 +128,7 @@ class Genres extends Taxonomy } ``` -## Column Order +## Column Positioning To rearrange columns pass an array of column slugs and position to the `order()` method. Only olumns you want to reorder need to be set, not all columns. @@ -152,5 +155,3 @@ class Genres extends Taxonomy } } ``` - - From 06c43985c93c06452c03d556630e0709dec821f8 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Mon, 29 Dec 2025 18:27:36 +0000 Subject: [PATCH 40/41] update documentation examples --- docs/taxonomies/create-columns.md | 2 +- docs/taxonomies/modify-columns.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/taxonomies/create-columns.md b/docs/taxonomies/create-columns.md index 552ebd8..a8e4fd3 100644 --- a/docs/taxonomies/create-columns.md +++ b/docs/taxonomies/create-columns.md @@ -20,7 +20,7 @@ class PopularityColumn extends Column */ public function name(): string { - return 'populariy'; + return 'popularity'; } /** diff --git a/docs/taxonomies/modify-columns.md b/docs/taxonomies/modify-columns.md index 909e9c8..0de82fd 100644 --- a/docs/taxonomies/modify-columns.md +++ b/docs/taxonomies/modify-columns.md @@ -28,11 +28,11 @@ class Genres extends Taxonomy // Position the column after the title column. ->after( 'title' ) // Populate the popularity column with term meta. - >populate( 'popularity', function( $term_id ) { + >populate( function( $term_id ) { echo get_term_meta( $term_id, '_popularity', true ); } ); // Make the popularity column sortable. - ->sortable( 'popularity', function( WP_Term_Query $query ) { + ->sort( function( WP_Term_Query $query ) { $query->query_vars['meta_key'] = '_popularity'; $query->query_vars['orderby'] = 'meta_value_num'; } ); @@ -72,7 +72,7 @@ class Genres extends Taxonomy ## Sortable Columns -To define which columns are sortable use the `sort()` method. +To define a column as sortable use the `sort()` method by passing in the sort callback. ```php use PostTypes\Taxonomy; From edd0aae747a4acbf10e51798f96ff26130a6837a Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Sat, 3 Jan 2026 22:03:58 +0000 Subject: [PATCH 41/41] update getting started code examples --- docs/Getting-started.md | 131 ---------------------------------------- docs/getting-started.md | 28 ++++++--- 2 files changed, 18 insertions(+), 141 deletions(-) delete mode 100644 docs/Getting-started.md diff --git a/docs/Getting-started.md b/docs/Getting-started.md deleted file mode 100644 index cd1b5bd..0000000 --- a/docs/Getting-started.md +++ /dev/null @@ -1,131 +0,0 @@ -# Getting Started - -## Requirements - -* PHP >=8.1 -* [Composer](https://getcomposer.org/) -* [WordPress](https://wordpress.org) >=6.3 - -## Installation - -#### Install with composer - -Run the following in your terminal to install PostTypes with [Composer](https://getcomposer.org/). - -``` -$ composer require jjgrainger/posttypes -``` - -PostTypes uses [PSR-4](https://www.php-fig.org/psr/psr-4/) autoloading and can be used with the Composer's autoloader. 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 - -#### Create a custom post type - -Custom post types are defined as classes that extend the base `PostType` class. At a minimum, the `name` method must be implemented to define the post type slug. All other methods are optional and allow you to configure labels, options, taxonomies, admin columns, filters, and more as needed. - -```php - __( 'Book', 'text-domain' ), - 'singular_name' => __( 'Book', 'text-domain' ), - 'menu_name' => __( 'Books', 'text-domain' ), - 'all_items' => __( 'Books', 'text-domain' ), - 'add_new' => __( 'Add New', 'text-domain' ), - 'add_new_item' => __( 'Add New Book', 'text-domain' ), - 'edit_item' => __( 'Edit Book', 'text-domain' ), - 'new_item' => __( 'New Book', 'text-domain' ), - 'view_item' => __( 'View Book', 'text-domain' ), - 'search_items' => __( 'Search Books', 'text-domain' ), - 'not_found' => __( 'No Books found', 'text-domain' ), - 'not_found_in_trash' => __( 'No Books found in Trash', 'text-domain' ), - 'parent_item_colon' => __( 'Parent Book', 'text-domain' ), - ]; - } - - /** - * Define Post Type feature supports. - */ - public function supports(): array { - return [ - 'title', - 'editor', - 'thumbnail', - 'custom-fields' - ]; - } - - /** - * Define Taxonomies associated with the Post Type. - */ - public function taxonomies(): array { - return [ - 'genre', - 'category' - ]; - } - - /** - * Set the menu icon for the Post Type. - */ - public function icon(): string { - return 'dashicons-book'; - } - - /** - * Set the admin post table filters. - */ - public function filters(): array { - return [ - 'genre', - 'category' - ]; - } - - /** - * Define the columns for the admin post table. - */ - public function columns(Columns $columns): Columns { - // Remove the author and date column. - $columns->remove( [ 'author', 'date' ] ); - - // Add a Rating column. - $columns->add( 'rating', __( 'Rating', 'post-types' ) ); - - // Populate the rating column. - $columns->populate( 'rating', function( $post_id ) { - echo get_post_meta( $post_id, 'rating', true ); - } ); - - return $columns; - } -} -``` - -### Register a custom post type - -Once the custom post type class is created it can be registered to WordPress by instantiating and call the register method. - -```php -// Instantiate the Book PostType class. -$book = new Book; - -// Register the Book PostType to WordPress. -$book->register(); -``` diff --git a/docs/getting-started.md b/docs/getting-started.md index cd1b5bd..7bd2dfd 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -67,7 +67,7 @@ class Book extends PostType { 'title', 'editor', 'thumbnail', - 'custom-fields' + 'custom-fields', ]; } @@ -77,7 +77,7 @@ class Book extends PostType { public function taxonomies(): array { return [ 'genre', - 'category' + 'category', ]; } @@ -94,7 +94,7 @@ class Book extends PostType { public function filters(): array { return [ 'genre', - 'category' + 'category', ]; } @@ -105,13 +105,21 @@ class Book extends PostType { // Remove the author and date column. $columns->remove( [ 'author', 'date' ] ); - // Add a Rating column. - $columns->add( 'rating', __( 'Rating', 'post-types' ) ); - - // Populate the rating column. - $columns->populate( 'rating', function( $post_id ) { - echo get_post_meta( $post_id, 'rating', true ); - } ); + // Add a new price column. + $columns->add( 'price' ) + // Set the label. + ->label( __( 'Price', 'my-text-domain' ) ) + // Position the column after the title column. + ->after( 'title' ) + // Set the populate callback. + ->populate( function( $post_id ) { + echo '$' . get_post_meta( $post_id, '_price', true ); + } ) + // Set the sort callback. + ->sort( function( WP_Query $query ) { + $query->set( 'meta_key', 'price' ); + $query->set( 'orderby', 'meta_value_num' ); + } ); return $columns; }