# PostTypes v3.0.1

[![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)

> 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. Please review the migration guide in the [documentation](https://posttypes.jjgrainger.co.uk) on how to upgrade to version 3.

## 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
<?php

use PostTypes\PostType;
use PostTypes\Columns;

class Book extends PostType {
    /**
     * Define the Post Type name.
     */
    public function name(): string {
        return 'book';
    }

    /**
     * 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' ),
        ];
    }

    /**
     * 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();
```

## Notes

* The full documentation can be found online at [posttypes.jjgrainger.co.uk](https://posttypes.jjgrainger.co.uk)
* Licensed under the [MIT License](https://github.com/jjgrainger/PostTypes/blob/master/LICENSE)
* Maintained under the [Semantic Versioning Guide](https://semver.org)

## Author

**Joe Grainger**

* <https://jjgrainger.co.uk>
* <https://twitter.com/jjgrainger>


# Migrating from v2 to v3

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 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 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
// Import PostTypes.
use PostTypes\PostType;

// Create a book post type.
$books = new PostType( 'book' );

// Hide the date and author columns.
$books->columns()->hide( [ 'date', 'author' ] );

// Set the Books menu icon.
$books->icon( 'dashicons-book-alt' );

// Register the post type to WordPress.
$books->register();
```

#### New PostType API

PostType is an abstract class and methods are used to configure the post type declaratively.

```php
namespace App\PostTypes;

use PostTypes\PostType;
use PostTypes\Columns;

class Books extends PostType {

    /**
     * Set the post type name.
     */
    public function name(): string {
        return 'book';
    }

    /**
     * Define post type columns.
     */
    public function columns( Columns $columns ): Columns {
        $columns->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 inside your theme functions.php or plugin file.

```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'          => __( 'Books', 'post-types' ),
            'singular_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;
use PostTypes\Taxonomy;

// Create a book post type.
$books = new PostType( 'book' );

// Attach the genre taxonomy (which is created below).
$books->taxonomy( 'genre' );

// Hide the date and author columns.
$books->columns()->hide( [ 'date', 'author' ] );

// Set the Books menu icon.
$books->icon( 'dashicons-book-alt' );

// Register the post type to WordPress.
$books->register();

// Create a genre taxonomy.
$genres = new Taxonomy( 'genre' );

// Set options for the taxonomy.
$genres->options( [
    'hierarchical' => false,
] );

// Register the taxonomy to WordPress.
$genres->register();
```

**v3.0:**

```php
use PostTypes\PostType;
use PostTypes\Taxonomy;

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';
    }
}


class Genre extends Taxonomy {
    public function name(): string {
        return 'genre';
    }

    public function options(): array {
        return [
            'hierarchical' => false,
        ];
    }
}

(new Book)->register();
(new Genre)->register();
```

***

## 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)


# 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
<?php

use PostTypes\PostType;
use PostTypes\Columns;

class Book extends PostType {
    /**
     * Define the Post Type name.
     */
    public function name(): string {
        return 'book';
    }

    /**
     * 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' ),
        ];
    }

    /**
     * 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 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;
    }
}
```

### 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();
```


# PostTypes

The following section contains information on creating and working with post types.

* [Create a Post Type](/post-types/create-a-post-type)
* [Define Labels](/post-types/define-labels)
* [Define Options](/post-types/define-options)
* [Define taxonomies](/post-types/define-taxonomies)
* [Define feature supports](/post-types/define-feature-supports)
* [Define an icon](https://github.com/jjgrainger/PostTypes/blob/main/docs/post-types/define-an-icon.md)
* [Define filters](/post-types/define-filters)
* [Modify columns](/post-types/modify-columns)
* [Create columns](/post-types/create-columns)
* [Define hooks](/post-types/define-hooks)


# 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 %}


# Define 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.

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' ),
        ];
    }
}
```


# Define Options

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.

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,
        ];
    }
}
```


# 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.

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) on creating a new taxonomy.

Taxonomies and post types can be created and registered in any order.


# Define feature supports

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).

```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',
        ];
    }
}
```


# Define an icon

[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.

```php
use PostTypes\PostType;

class Books extends PostType
{
    //...

    /**
     * Returns the admin menu icon for the Books post type.
     *
     * @return string
     */
    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/)


# Define filters

Filters that appear for the post type listing admin screen can be defined using the `filters()` method.

This must return an array of taxonomy slugs that are to be used as dropdown filters for the post type.

By default, an empty array is returned.

```php
use PostTypes\PostType;

class Books extends PostType
{
    //...

    /**
     * Returns the filters for the Books post type.
     *
     * @return array
     */
    public function filters(): array
    {
        return [
            'category',
        ];
    }
}
```


# Modify 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

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;
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->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;
    }
}
```

## 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 a column use the `populate()` method passing 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 $columns ): Columns
    {
        $columns->add( 'price' )->populate( function( $post_id ) {
            echo '$' . get_post_meta( $post_id, '_price', true );
        } );

        return $columns;
    }
}
```

## Sortable Columns

To make a column sortable use the `sort()` method and pass the sorting callback.

```php
use PostTypes\PostType;
use PostTypes\Columns;

class Books extends PostType
{
    //...

    /**
     * Set the PostTypes admin columns.
     *
     * @return array
     */
    public function columns( Columns $columns ): Columns
    {
        // Make the rating column sortable.
        $columns->add( 'price' )->sort( function( WP_Query $query ) {
            $query->set( 'meta_key', 'price' );
            $query->set( 'orderby', 'meta_value_num' );
        } );

        return $columns;
    }
}
```

## Remove Columns

To remove columns pass the column slug to the `remove()` 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 $columns ): Columns
    {
        // Hide the Author and Date columns
        $columns->remove( [ 'author', 'date' ] );

        return $columns;
    }
}
```

## Whitelist Columns

Use the `only()` method to define what columns should appear by passing 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 $columns ): Columns
    {
        // 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;
    }
}
```


# 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 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.

## 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
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' );
    }

    /**
     * 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 $post_id ) {
            echo '$' . get_post_meta( $post_id, '_price', true );
        };
    }

    /**
     * Handle sorting the column by modifying the admin query.
     *
     * @return callable
     */
    public function sort(): callable
    {
        return function( \WP_Query $query ) {
            $query->set( 'meta_key', '_price' );
            $query->set( 'orderby', 'meta_value_num' );
        };
    }
}
```

## 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` or `Taxonomy` class:

```php
use PostTypes\PostType;

class Book extends PostType
{
    //...

    public function columns( Columns $columns ): Columns
    {
        $columns->column( new PriceColumn );

        return $columns;
    }
}
```


# Define 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 void
     */
    public function hooks(): void
    {
        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...
    }
}
```


# Taxonomies

The following section contains information on creating and working with taxonomies.

* [Create a Taxonomy](/taxonomies/create-a-taxonomy)
* [Define Labels](/taxonomies/define-labels)
* [Define Options](/taxonomies/define-options)
* [Define Post Types](/taxonomies/define-post-types)
* [Modify Columns](/taxonomies/modify-columns)
* [Create Columns](https://github.com/jjgrainger/PostTypes/blob/main/docs/taxonomies/create-columns.md)
* [Define Hooks](/taxonomies/define-hooks)


# Create a Taxonomy

Taxonomies are created using the `Taxonomy` class. This works identically to the `PostType` class and holds similar methods.

## Create a new taxonomy

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;

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 %}


# Define 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.

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' ),
        ];
    }
}
```


# Define Options

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.

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,
        ];
    }
}
```


# Define Post Types

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.

```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) on creating a new post type.

Taxonomies and post types can be created and registered in any order.


# 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.

## Add Columns

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;
use PostTypes\Columns;

class Genres extends Taxonomy
{
    //...

    /**
     * Set the Taxonomy admin columns.
     *
     * @return Columns
     */
    public function columns( Columns $columns ): Columns
    {
        // Add a new Popularity column.
        $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( function( $term_id ) {
                echo get_term_meta( $term_id, '_popularity', true );
            } );
            // Make the popularity column sortable.
            ->sort( function( WP_Term_Query $query ) {
                $query->query_vars['meta_key'] = '_popularity';
                $query->query_vars['orderby'] = 'meta_value_num';
            } );

        return $columns;
    }
}
```

## Populate Columns

To populate any column use the `populate()` method and passing 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 $columns ): Columns
    {
        $columns->add( 'popularity' )->populate( function( $term_id ) {
            echo get_term_meta( $term_id, '_popularity', true );
        } );

        return $columns;
    }
}
```

## Sortable Columns

To define a column as sortable use the `sort()` method by passing in the sort callback.

```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 $columns ): Columns
    {
        // Make the popularity column sortable.
        $columns->add( 'popularity' )->sort( function( WP_Term_Query $query ) {
            $query->query_vars['meta_key'] = '_popularity';
            $query->query_vars['orderby'] = 'meta_value_num';
        } );

        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 $columns ): Columns
    {
        // Hide the Description column.
        $columns->hide( [ 'description' ] );

        return $columns;
    }
}
```

## 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.

```php
use PostTypes\Taxonomy;
use PostTypes\Columns;

class Genres extends Taxonomy
{
    //...

    /**
     * Set the Taxonomy admin columns.
     *
     * @return array
     */
    public function columns( Columns $columns ): Columns
    {
        // Position the new Popularity column.
        $columns->position( 'popularity', 'after', 'title' );

        return $columns;
    }
}
```


# Define 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 taxonomy in one class.

```php
use PostTypes\Taxonomy;

class Genres extends Taxonomy
{
    //...

    /**
     * Adds additional hooks for the taxonomy.
     *
     * @return void
     */
    public function hooks(): void
    {
        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...
    }
}
```


# Contributing

First, thank you for taking the time to contribute to [PostTypes](https://github.com/jjgrainger/PostTypes), you're amazing! 🎉 👏 🙌

All contributions are welcome and appreciated. It is recommended that you read through this document before making your first contribution.

The following instructions are recommended as *guidelines* and not strict rules. However, following this guide will make it easier for both you and the maintainers when working on the project.

**There are 3 ways you can contribute to PostTypes.**

* [Create an Issue](#create-an-issue)
* [Submit a Pull Request](#submit-a-pull-request)
* [Show Support](#show-support)

## Purpose

PostTypes mission is to *create advanced WordPress custom post types easily*.

Its focus is on the creation of custom post types, specifically around the admin interface, providing methods to create advanced post tables, columns, filters and more.

It also makes it easy to create taxonomies and assign them to post types.

Although its focus is on the admin interface, PostTypes **is not for custom fields**. There are plenty of solutions for creating custom fields in WordPress. PostTypes has no intention of being one.

Please keep this in mind when contributing to the project.

## Create an Issue

If you would like to report a bug, feature request, documentation improvement or question please [create an issue](https://github.com/jjgrainger/PostTypes/issues/new). Before creating a new issue please check it has not already been raised by searching [issues](https://github.com/jjgrainger/PostTypes/issues) on GitHub.

When creating an issue it is best to provide as much information as possible in order to help the discussion move quickly and efficiently.

**There are 4 types of issues:**

* [Bug Reports](#bug-report)
* [Feature Requests](#feature-request)
* [Documentation Improvements](#documentation-improvement)
* [Support Questions](#support-questions)

### 🐛 Bug Report

Bug reports highlight an error or unexpected behaviour when using the code. In order to resolve the issue, enough detail must be provided in order to recreate the problem so it can be investigated and fixed.

**Tips on creating bug reports:**

* Provide as much detail about the problem.
* Give steps to help reproduce the problem.
* Code examples and error messages are useful.
* Version numbers (PHP, PostTypes, WordPress) are useful.

### 🚀 Feature Request

Feature requests suggest an idea for an improvement or additional functionality. Feature requests should be raised **before submitting a** [**pull request**](#submit-a-pull-request). This provides an opportunity for discussion and help prevent unnecessary work from being carried out.

**Tips on creating feature requests:**

* Provide a description of the change you want to make.
* Highlight the problem it attempts to solve.
* Offer examples of how it would be used.
* Provide links to WordPress documentation where relevant.

### 📖 Documentation Improvements

Documentation improvements suggest ways to enhance the [documentation](https://posttypes.jjgrainger.co.uk). This could be anything from fixing spelling errors to adding new sections.

**Tips on creating documentation improvements:**

* Provide links to the pages you're referring to.
* Offer an explanation on how this is an improvement.

### 🎈 Support Questions

For general questions and support. This is also a catch-all for anything that doesn't fit in the categories above.

**Tips on creating support questions:**

* Please check the [documentation](https://posttypes.jjgrainger.co.uk) first for your answer.
* Provide code examples if necessary.
* Keep questions relevant to PostTypes.

### Labels

Labels are used to help organise different types of issues. Labels are prefixed with their group, which currently are *type* and *status*. Maintainers will apply the correct labels to an issue when they are reviewed.

| Label                   | Description                                                    |
| ----------------------- | -------------------------------------------------------------- |
| **Type: Bug**           | Bug reports and issues with unexpected behaviour               |
| **Type: Feature**       | Feature requests and ideas                                     |
| **Type: Docs**          | Improvements and fixes around documentation                    |
| **Type: Support**       | General support and questions                                  |
| **Status: Discussion**  | An issue that needs discussion before it can be worked on.     |
| **Status: Ready**       | An issue that is ready to be picked up.                        |
| **Status: In Progress** | An issue that is currently being worked.                       |
| **Status: Review**      | An issue that is finished and ready to be reviewed and merged. |
| **Status: Complete**    | An issue that is finished and merged into master.              |

This is not an exhaustive list. Labels may be changed and new ones created over time. For a complete list, see the [GitHub repo](https://github.com/jjgrainger/PostTypes/labels).

## Submit a Pull Request

**Before submitting a Pull Request** it is recommended you [create an issue](#create-an-issue) first. This provides an opportunity to open up a discussion before any work takes place.

### Basic Workflow

1. [Create a Fork](https://guides.github.com/activities/forking/#fork) of the main [jjgrainger/PostTypes](https://github.com/jjgrainger/PostTypes) repository.
2. [Clone your fork](https://guides.github.com/activities/forking/#clone) locally to your machine.
3. Create a branch with one of the relevant [branch prefixes](#branch-prefixes).
4. Commit work to your branch.
5. Push your branch to your fork on GitHub.
6. Create a [pull request](https://github.com/jjgrainger/PostTypes/compare) comparing your forks branch against the main repository's `master`.

**Tips on creating pull requests:**

* [Reference issue numbers](https://help.github.com/articles/closing-issues-using-keywords/) in your commits where applicable.
* Commit *little and often*, smaller commits make it easier to review code.
* **Do not** include issue numbers in the title of your pull request.
* Please provide a description with your pull request with details about the change you are trying to make.
* Please link to any issues the pull request is related.

### Branch Prefixes

Branch prefixes are used to help categorise the working branches by type.

| Prefix     | Description                                | Example                        |
| ---------- | ------------------------------------------ | ------------------------------ |
| `fix/`     | An attempt to fix a bug                    | `fix/incorrect-post-type-name` |
| `feature/` | A new feature being worked on              | `feature/bulk-update-actions`  |
| `docs/`    | Documentation fix, addition or improvement | `docs/fix-spelling-errors`     |
| `release/` | A release branch to create a new release.  | `release/v2.0.1`               |

The `release/` prefix is to be used by **maintainers only**.

The `docs/` prefix is used alongside [Gitbook](https://www.gitbook.com/) where the docs are hosted. Branches with this prefix will be automatically generated and made available as a version on the [documentation site](https://posttypes.jjgrainger.co.uk). This provides an opportunity to preview changes and approve them before being merged.

## Show Support

PostTypes is open source and free to use. It was created to solve a problem while giving something back to the community. There are many ways to show your support, some ideas include:

* Creating an [issue](https://github.com/jjgrainger/PostTypes/issues/new) and [pull requests](https://github.com/jjgrainger/PostTypes/compare).
* [Staring](https://github.com/jjgrainger/PostTypes/stargazers) the project on GitHub.
* Saying "thank you" over on [Twitter](https://twitter.com/jjgrainger)
* Spread the word and let others know.
* [Buy me a beer](https://www.paypal.me/jjgrainger/5).


# Changelog

**v3.0.1**

* merge [pull request #109](https://github.com/jjgrainger/PostTypes/pull/109): Check taxonomy in query vars is an array
* maintenance: Update PHPUnit and GitHub workflows to php 8.4

**v3.0.0**

* merge [pull request #98](https://github.com/jjgrainger/PostTypes/pull/98): Major v3.0 refactor and feature updates
* refactor: modernise architecture and introduce class-based PostType/Taxonomy handling
* feature: improved column handling and introduce column builder
* docs: update examples and documentation for v3.0 implementation
* maintenance: introduce phpstan and improve GitHub workflows

**v2.2.2**

* merge [pull request #103](https://github.com/jjgrainger/PostTypes/pull/103): Update documentation for translations

**v2.2.1**

* merge [pull requests #95](https://github.com/jjgrainger/PostTypes/pull/95): Update tests
* merge [pull requests #93](https://github.com/jjgrainger/PostTypes/pull/93): Fix empty array if taxonomy query var is null
* merge [pull requests #88](https://github.com/jjgrainger/PostTypes/pull/88): Fix docblock definition

**v2.2**

* merge [pull requests #81](https://github.com/jjgrainger/PostTypes/pull/81): Fix Taxonomy dropdown filter
* merge [pull requests #80](https://github.com/jjgrainger/PostTypes/pull/80): Fix Modify Existing Objects
* merge [pull requests #72](https://github.com/jjgrainger/PostTypes/pull/72): fix issue 71
* merge [pull requests #74](https://github.com/jjgrainger/PostTypes/pull/74): Allow setting false to sort by alphabetical
* merge [pull requests #79](https://github.com/jjgrainger/PostTypes/pull/79): Update docs and README examples
* merge [pull requests #78](https://github.com/jjgrainger/PostTypes/pull/78): Maintenance

**v2.1**

* merge [pull reqeuest #45](https://github.com/jjgrainger/PostTypes/pull/45): add PHP 7.2 Compatibility
* merge [pull reqeuest #46](https://github.com/jjgrainger/PostTypes/pull/46): Make sure the "orderby" query var is a string when sorting by columns.
* merge [pull reqeuest #55](https://github.com/jjgrainger/PostTypes/pull/55): Fix column mismatch broken sort
* merge [pull reqeuest #56](https://github.com/jjgrainger/PostTypes/pull/56): Grammar mistakes corrected
* merge [pull reqeuest #61](https://github.com/jjgrainger/PostTypes/pull/61): Fix incorrect property types
* merge [pull reqeuest #62](https://github.com/jjgrainger/PostTypes/pull/62): Allow for multiple taxonomies and post types to be added
* merge [pull reqeuest #63](https://github.com/jjgrainger/PostTypes/pull/63): Update Taxonomy columns documentation
* merge [pull reqeuest #64](https://github.com/jjgrainger/PostTypes/pull/64): Update minimum PHP version, phpunit and phpcs

**v2.0.1**

* merge [pull reqeuest #19](https://github.com/jjgrainger/PostTypes/pull/19): Use wp\_dropdown\_categories function for post filtering selectbox
* update minimum php version to 5.6

**v2.0**

* fix [issue #9](https://github.com/jjgrainger/PostTypes/issues/9): add unit tests
* fix [issue #12](https://github.com/jjgrainger/PostTypes/issues/12) and [issue #13](https://github.com/jjgrainger/PostTypes/issues/13): generating duplicate columns
* fix [issue #2](https://github.com/jjgrainger/PostTypes/issues/2) and [issue #16](https://github.com/jjgrainger/PostTypes/issues/16): translations not working
* create Taxonomy class
* fix [issue #11](https://github.com/jjgrainger/PostTypes/issues/11): add `columns()` to Taxonomy class
* update [`examples/books.php`](https://github.com/jjgrainger/PostTypes/blob/master/examples/books.php)

**v1.1.2**

* fix PHPCS as dev requirement
* add version to composer json

**v1.1.1**

* fix [issue #8](https://github.com/jjgrainger/PostTypes/issues/8): Error with `$addTaxonomy`

**v1.1**

* fix [issue #6](https://github.com/jjgrainger/PostTypes/issues/6): problem registering existing taxonomies
* add [issue #1](https://github.com/jjgrainger/PostTypes/issues/1): Add PHPCS and Travic integration
* merge [pull request #3](https://github.com/jjgrainger/PostTypes/pull/3): Add `labels()` method to `PostTypes`


