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/.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/README.md b/README.md index 782016a..07aba50 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ -# 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. Please review the migration guide in the [documentation](https://posttypes.jjgrainger.co.uk) on how to upgrade to version 3. ## 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,64 +24,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. - -```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. For more information, check out the [online documentation here](https://posttypes.jjgrainger.co.uk). +#### 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'; +taxonomy( 'genre' ); - -// Hide the date and author columns. -$books->columns()->hide( [ 'date', 'author' ] ); - -// Set the Books menu icon. -$books->icon( 'dashicons-book-alt' ); +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 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 Book PostType class. +$book = new Book; -// Register the taxonomy to WordPress. -$genres->register(); +// 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) -* 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/composer.json b/composer.json index 011d3a1..985d10b 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": { @@ -27,11 +30,15 @@ } }, "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", - "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/docs/Getting-started.md b/docs/Getting-started.md deleted file mode 100644 index 628a53e..0000000 --- a/docs/Getting-started.md +++ /dev/null @@ -1,70 +0,0 @@ -# Getting Started - -## Requirements - -* PHP >=7.2 -* [Composer](https://getcomposer.org/) -* [WordPress](https://wordpress.org) >=5.1 - -## Installation - -#### Install with composer - -Run the following in your terminal to install PostTypes with [Composer](https://getcomposer.org/). - -``` -$ composer require 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. - -## Basic Usage - -Below is a basic example of setting up a simple book post type with a genre taxonomy. - -```php -// Require the Composer autoloader. -require __DIR__ . '/vendor/autoload.php'; - -// Import PostTypes. -use PostTypes\PostType; -use PostTypes\Taxonomy; - -// Create a book post type. -$books = new PostType( 'book' ); - -// Attach the genre taxonomy (which is created below). -$books->taxonomy( 'genre' ); - -// Hide the date and author columns. -$books->columns()->hide( [ 'date', 'author' ] ); - -// Set the Books menu icon. -$books->icon( 'dashicons-book-alt' ); - -// Register the post type to WordPress. -$books->register(); - -// Create a genre taxonomy. -$genres = new Taxonomy( 'genre' ); - -// Set options for the taxonomy. -$genres->options( [ - 'hierarchical' => false, -] ); - -// Register the taxonomy to WordPress. -$genres->register(); -``` 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 0ce1fb5..f27d146 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,18 +1,25 @@ # Table of Contents -* [PostTypes v2.2.1](../README.md) -* [Getting Started](Getting-started.md) +* [PostTypes v3.0](../README.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) - * [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) + * [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) - * [Add to Post Type](taxonomies/Add-to-post-type.md) - * [Columns](taxonomies/Columns.md) -* [Notes](Notes.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/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..7bd2dfd --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,139 @@ +# 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 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(); +``` diff --git a/docs/migrating-from-v2-v3.md b/docs/migrating-from-v2-v3.md new file mode 100644 index 0000000..bc8f8b9 --- /dev/null +++ b/docs/migrating-from-v2-v3.md @@ -0,0 +1,288 @@ +# Migrating from PostTypes v2.x to v3.0 + +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) 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..183b014 100644 --- a/docs/post-types/Create-a-post-type.md +++ b/docs/post-types/Create-a-post-type.md @@ -1,119 +1,36 @@ # 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(); +class Books extends PostType +{ + /** + * Returns the post type name to register to WordPress. + * + * @return string + */ + public function name(): string + { + return 'book'; + } +} ``` -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. +## Register PostType to WordPress -### Set options - -Options for the post type are set by passing an array as the second argument in the class constructor. +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 -$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, -] ); +// Instantiate the Books PostType class. +$books = new Books; +// Register the books PostType to WordPress. $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(); -``` - -Alternatively, you can use the `labels()` method to set the labels for the post type. - -```php -$books = new PostType( 'books' ); - -$books->labels( [ - 'add_new_item' => __( 'Add new Book' ), -] ); - -$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/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/README.md b/docs/post-types/README.md index 380a23c..118be5b 100644 --- a/docs/post-types/README.md +++ b/docs/post-types/README.md @@ -2,9 +2,13 @@ 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](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/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/create-columns.md b/docs/post-types/create-columns.md new file mode 100644 index 0000000..6bad4a2 --- /dev/null +++ b/docs/post-types/create-columns.md @@ -0,0 +1,91 @@ +# 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; + } +} +``` diff --git a/docs/post-types/define-feature-supports.md b/docs/post-types/define-feature-supports.md new file mode 100644 index 0000000..ea9a662 --- /dev/null +++ b/docs/post-types/define-feature-supports.md @@ -0,0 +1,28 @@ +## 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', + ]; + } +} +``` diff --git a/docs/post-types/define-filters.md b/docs/post-types/define-filters.md new file mode 100644 index 0000000..3ffd179 --- /dev/null +++ b/docs/post-types/define-filters.md @@ -0,0 +1,28 @@ +# 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', + ]; + } +} +``` diff --git a/docs/post-types/define-hooks.md b/docs/post-types/define-hooks.md new file mode 100644 index 0000000..e287c75 --- /dev/null +++ b/docs/post-types/define-hooks.md @@ -0,0 +1,38 @@ +# 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... + } +} +``` diff --git a/docs/post-types/define-icon.md b/docs/post-types/define-icon.md new file mode 100644 index 0000000..9ca43bb --- /dev/null +++ b/docs/post-types/define-icon.md @@ -0,0 +1,26 @@ +# Menu Icons + +[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/) diff --git a/docs/post-types/define-labels.md b/docs/post-types/define-labels.md new file mode 100644 index 0000000..7b8f911 --- /dev/null +++ b/docs/post-types/define-labels.md @@ -0,0 +1,39 @@ +# 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' ), + ]; + } +} +``` diff --git a/docs/post-types/define-options.md b/docs/post-types/define-options.md new file mode 100644 index 0000000..528ecdb --- /dev/null +++ b/docs/post-types/define-options.md @@ -0,0 +1,29 @@ +# 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, + ]; + } +} +``` diff --git a/docs/post-types/define-taxonomies.md b/docs/post-types/define-taxonomies.md new file mode 100644 index 0000000..8ec10c8 --- /dev/null +++ b/docs/post-types/define-taxonomies.md @@ -0,0 +1,31 @@ +# 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.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 new file mode 100644 index 0000000..2826638 --- /dev/null +++ b/docs/post-types/modify-columns.md @@ -0,0 +1,255 @@ +# 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; + } +} +``` + 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..7507660 100644 --- a/docs/taxonomies/Create-a-taxonomy.md +++ b/docs/taxonomies/Create-a-taxonomy.md @@ -4,110 +4,60 @@ 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; -// 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/README.md b/docs/taxonomies/README.md index af22a96..af91b69 100644 --- a/docs/taxonomies/README.md +++ b/docs/taxonomies/README.md @@ -2,6 +2,10 @@ 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) +* [Create a Taxonomy](create-a-taxonomy.md) +* [Define Labels](define-labels.md) +* [Define Options](define-options.md) +* [Define Post Types](define-post-types.md) +* [Modify Columns](modify-columns.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 new file mode 100644 index 0000000..7507660 --- /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 + +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 %} diff --git a/docs/taxonomies/create-columns.md b/docs/taxonomies/create-columns.md new file mode 100644 index 0000000..a8e4fd3 --- /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 'popularity'; + } + + /** + * 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 new file mode 100644 index 0000000..8d9028d --- /dev/null +++ b/docs/taxonomies/define-hooks.md @@ -0,0 +1,44 @@ +# 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... + } +} +``` diff --git a/docs/taxonomies/define-labels.md b/docs/taxonomies/define-labels.md new file mode 100644 index 0000000..98ad264 --- /dev/null +++ b/docs/taxonomies/define-labels.md @@ -0,0 +1,33 @@ +# 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' ), + ]; + } +} +``` diff --git a/docs/taxonomies/define-options.md b/docs/taxonomies/define-options.md new file mode 100644 index 0000000..0dceafc --- /dev/null +++ b/docs/taxonomies/define-options.md @@ -0,0 +1,29 @@ +# 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, + ]; + } +} +``` diff --git a/docs/taxonomies/define-post-types.md b/docs/taxonomies/define-post-types.md new file mode 100644 index 0000000..46ea59b --- /dev/null +++ b/docs/taxonomies/define-post-types.md @@ -0,0 +1,31 @@ +# 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.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 new file mode 100644 index 0000000..0de82fd --- /dev/null +++ b/docs/taxonomies/modify-columns.md @@ -0,0 +1,157 @@ +# 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; + } +} +``` 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; + } +} diff --git a/examples/Genres.php b/examples/Genres.php new file mode 100644 index 0000000..1d46939 --- /dev/null +++ b/examples/Genres.php @@ -0,0 +1,60 @@ +remove( [ 'posts' ] ); + + $columns->label( 'popularity', __( 'Popularity', 'post-types' ) ); + + $columns->populate( 'popularity', function( $term_id ) { + echo get_term_meta( $term_id, 'popularity', true ); + } ); + + $columns->sort( '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..f161c1e --- /dev/null +++ b/examples/Price.php @@ -0,0 +1,31 @@ +after( 'title' ); + } + + public function populate(): callable { + return function( int $post_id ) { + echo '£' . get_post_meta( $post_id, 'price', true ); + }; + } + + 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 8451431..13d2b9a 100644 --- a/examples/books.php +++ b/examples/books.php @@ -1,59 +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', + ]; + } -// Create a books Post Type -$books = new PostType( 'book' ); + public function supports(): array { + return [ + 'title', + 'editor', + 'author', + 'custom-fields', + ]; + } -// Add the Genre Taxonomy -$books->taxonomy( 'genre' ); + public function options(): array { + return [ + 'show_in_rest' => false, + ]; + } -// Hide the date and author columns -$books->columns()->hide( [ 'date', 'author' ] ); + public function icon(): string { + return 'dashicons-book'; + } -// add a price and rating column -$books->columns()->add( [ - 'rating' => __( 'Rating' ), - 'price' => __( 'Price' ) -] ); + public function filters(): array { + return [ + 'genre', + 'post_tag', + ]; + } -// Populate the custom column -$books->columns()->populate( 'rating', function( $column, $post_id ) { - echo get_post_meta( $post_id, 'rating' ) . '/10'; -} ); + public function columns( Columns $columns ): Columns { -// Populate the custom column -$books->columns()->populate( 'price', function( $column, $post_id ) { - echo '£' . get_post_meta( $post_id, 'price' ); -} ); + $columns->remove( [ 'author', 'date' ] ); -// Set sortable columns -$books->columns()->sortable( [ - 'price' => [ 'price', true ], - 'rating' => [ 'rating', true ] -] ); + $columns->column( new Price ); -// Set the Books menu icon -$books->icon( 'dashicons-book-alt' ); + $columns->label( 'rating', __( 'Rating', 'post-types' ) ); -// Register the PostType to WordPress -$books->register(); + $columns->position( 'rating', 'after', 'price' ); -// 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->sort( '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->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'); + } ); -// Register the taxonomy to WordPress -$genres->register(); + return $columns; + } +} 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 diff --git a/src/Column.php b/src/Column.php new file mode 100644 index 0000000..8e3ec39 --- /dev/null +++ b/src/Column.php @@ -0,0 +1,77 @@ +name())); + } + + /** + * Populate the column. + * + * @return callable|null + */ + public function populate(): ?callable + { + return null; + } + + /** + * Set the column order. + * + * @return array|null + */ + public function position(): ?array + { + return null; + } + + /** + * Return the sort callback for the column. + * + * @return callable|null + */ + public function sort(): ?callable + { + return null; + } + + /** + * Return the before position array structure. + * + * @param string $reference + * @return array + */ + protected function before(string $reference): array + { + return ['before', $reference]; + } + + /** + * Return the after position array structure. + * + * @param string $reference + * @return array + */ + protected function after(string $reference): array + { + return ['after', $reference]; + } +} diff --git a/src/ColumnBuilder.php b/src/ColumnBuilder.php new file mode 100644 index 0000000..5dc823d --- /dev/null +++ b/src/ColumnBuilder.php @@ -0,0 +1,107 @@ +columns = $columns; + $this->key = $key; + } + + /** + * Set the label for the column. + * + * @param string $label + * @return ColumnBuilder + */ + public function label(string $label): ColumnBuilder + { + $this->columns->label($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->columns->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->columns->populate($this->key, $callback); + + return $this; + } + + /** + * Set columns sort callback. + * + * @param callable $callback + * @return ColumnBuilder + */ + public function sort(callable $callback): ColumnBuilder + { + $this->columns->sort($this->key, $callback); + + return $this; + } +} diff --git a/src/Columns.php b/src/Columns.php index 14ac23d..709622d 100644 --- a/src/Columns.php +++ b/src/Columns.php @@ -2,239 +2,245 @@ 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 InvalidArgumentException; +use PostTypes\Contracts\ColumnContract; + class Columns { /** - * Holds an array of all the defined columns. + * Columns keys and labels. * * @var array */ - public $items = []; + protected $labels = []; /** - * An array of columns to add. + * Columns to remove. * * @var array */ - public $add = []; + protected $remove = []; /** - * An array of columns to hide. + * Columns whitelist. * * @var array */ - public $hide = []; + protected $only = []; /** - * An array of columns to reposition. + * Column positions. * * @var array */ - public $positions = []; + protected $positions = []; /** - * An array of custom populate callbacks. + * Column populate callbacks. * * @var array */ - public $populate = []; + protected $populateCallbacks = []; /** - * An array of columns that are sortable. + * Sortable columns and sort callbacks. * * @var array */ - public $sortable = []; + protected $sortCallbacks = []; /** - * Set the all columns - * @param array $columns an array of all the columns to replace + * Create a new Column. + * + * @param string $key + * @return ColumnBuilder */ - public function set($columns) + public function add(string $key): ColumnBuilder { - $this->items = $columns; + return new ColumnBuilder($this, $key); } /** - * Add a new column - * @param string $column the slug of the column - * @param string $label the label for the column + * Modify an existing column. + * + * @param string $key + * @return ColumnBuilder */ - public function add($columns, $label = null) + public function modify(string $key): ColumnBuilder { - - if (!is_array($columns)) { - $columns = [$columns => $label]; - } - - foreach ($columns as $column => $label) { - if (is_null($label)) { - $label = str_replace(['_', '-'], ' ', ucfirst($column)); - } - - $this->add[$column] = $label; - } - - return $this; + return $this->add($key); } /** - * Add a column to hide - * @param string $column the slug of the column to hdie + * Add a column object. + * + * @param ColumnContract $column + * @return void */ - public function hide($columns) + public function column(ColumnContract $column): void { - if (!is_array($columns)) { - $columns = [$columns]; + $this->label($column->name(), $column->label()); + + if (!is_null($column->position())) { + [$direction, $reference] = $column->position(); + + $this->position($column->name(), $direction, $reference); } - foreach ($columns as $column) { - $this->hide[] = $column; + if ($callback = $column->populate()) { + $this->populate($column->name(), $callback); } - return $this; + if ($callback = $column->sort()) { + $this->sort($column->name(), $callback); + } } /** - * Set a custom callback to populate a column - * @param string $column the column slug - * @param mixed $callback callback function + * Remove columns. + * + * @param array $keys + * @return void */ - public function populate($column, $callback) + public function remove(array $keys): void { - $this->populate[$column] = $callback; - - return $this; + $this->remove = array_merge($this->remove, $keys); } /** - * Define the postion for a columns - * @param string $columns an array of columns + * Set columns. + * + * @param array $keys + * @return void */ - public function order($columns) + public function only(array $keys): void { - foreach ($columns as $column => $position) { - $this->positions[$column] = $position; - } - - return $this; + $this->only = array_merge($this->only, $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 the label for a column. + * + * @param string $key + * @param string $label + * @return void */ - public function sortable($sortable) + public function label(string $key, string $label): void { - foreach ($sortable as $column => $options) { - $this->sortable[$column] = $options; - } - - return $this; + $this->labels[$key] = $label; } /** - * Check if an orderby field is a custom sort option. - * @param string $orderby the orderby value from query params + * Set column position. + * + * @param string $key + * @param string $direction + * @param string $reference + * @return void + * @throws InvalidArgumentException */ - public function isSortable($orderby) + public function position(string $key, string $direction, string $reference): void { - if (is_string($orderby) && array_key_exists($orderby, $this->sortable)) { - return true; + if (!in_array($direction, ['before', 'after'], true)) { + throw new InvalidArgumentException("Invalid position direction '{$direction}'"); } - 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; - } - } + $this->positions[$key] = [$direction, $reference]; + } - return false; + /** + * Set column populate callback. + * + * @param string $key + * @param callable $callback + * @return void + */ + public function populate(string $key, callable $callback): void + { + $this->populateCallbacks[$key] = $callback; } /** - * Get meta key for an orderby. - * @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 sortableMeta($orderby) + public function sort(string $key, callable $callback): void { - if (array_key_exists($orderby, $this->sortable)) { - return $this->sortable[$orderby]; - } + $this->sortCallbacks[$key] = $callback; + } - 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; - } - } + /** + * Get columns to add. + * + * @return array + */ + public function getColumns(): array + { + return $this->labels; + } - return ''; + /** + * Get removed columns. + * + * @return array + */ + public function getRemoved(): array + { + return $this->remove; } /** - * Modify the columns for the object - * @param array $columns WordPress default columns - * @return array The modified columns + * Get only columns. + * + * @return array */ - public function modifyColumns($columns) + public function getOnly(): array { - // if user defined set columns, return those - if (!empty($this->items)) { - return $this->items; - } + return $this->only; + } - // add additional columns - if (!empty($this->add)) { - foreach ($this->add as $key => $label) { - $columns[$key] = $label; - } - } + /** + * Get column positions. + * + * @return array + */ + public function getPositions(): array + { + return $this->positions; + } - // unset hidden columns - if (!empty($this->hide)) { - foreach ($this->hide as $key) { - unset($columns[$key]); - } - } + /** + * Get a column populate callback. + * + * @param string $key + * @return callable|null + */ + public function getPopulateCallback(string $key): ?callable + { + return $this->populateCallbacks[$key] ?? null; + } - // 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; - } - } + /** + * Get sortable columns. + * + * @return array + */ + public function getSortableColumns(): array + { + return array_combine(array_keys($this->sortCallbacks), array_keys($this->sortCallbacks)); + } - return $columns; + /** + * Get column sort callback. + * + * @param string $key + * @return callable|null + */ + public function getSortCallback(string $key): ?callable + { + return $this->sortCallbacks[$key] ?? null; } } diff --git a/src/Contracts/ColumnContract.php b/src/Contracts/ColumnContract.php new file mode 100644 index 0000000..12fa9c1 --- /dev/null +++ b/src/Contracts/ColumnContract.php @@ -0,0 +1,41 @@ +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(); } } diff --git a/src/Registrars/PostTypeRegistrar.php b/src/Registrars/PostTypeRegistrar.php index 898e41f..2a5703b 100644 --- a/src/Registrars/PostTypeRegistrar.php +++ b/src/Registrars/PostTypeRegistrar.php @@ -2,220 +2,260 @@ namespace PostTypes\Registrars; -use PostTypes\PostType; +use PostTypes\Contracts\PostTypeContract; +use PostTypes\Columns; class PostTypeRegistrar { - protected $posttype; + /** + * PostType to register. + * + * @var PostTypeContract + */ + private $posttype; - public function __construct(PostType $posttype) + /** + * The PostType columns. + * + * @var Columns + */ + private $columns; + + /** + * Constructor. + * + * @param PostTypeContract $posttype + */ + public function __construct(PostTypeContract $posttype) { $this->posttype = $posttype; } + /** + * 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 { - add_filter('register_post_type_args', [$this, 'modifyPostType'], 10, 2); - } + // Initialize the post type. + add_action('init', [$this, 'createColumns'], 10, 0); + add_action('init', [$this, 'initialize'], 10, 0); - // register Taxonomies to the PostType - add_action('init', [$this, 'registerTaxonomies'], 10); - - // modify filters on the admin edit screen + // Handle PostType filters. 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 columns. + add_filter('manage_' . $name . '_posts_columns', [$this, 'modifyColumns'], 10, 1); + 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); - // run filter to make columns sortable. - add_filter('manage_edit-' . $name . '_sortable_columns', [$this, 'setSortableColumns'], 10, 1); + // Register custom hooks. + $this->posttype->hooks(); + } - // run action that sorts columns on request. - add_action('pre_get_posts', [$this, 'sortSortableColumns'], 10, 1); - } + /** + * Create Columns. + * + * @return void + */ + public function createColumns() + { + $this->columns = $this->posttype->columns(new Columns()); } /** - * Register the PostType + * Register Post Type. + * * @return void */ - public function registerPostType() + public function initialize() { - // create options for the PostType - $options = $this->posttype->createOptions(); + // Modify the existing PostType if it exists. + if (post_type_exists($this->posttype->name())) { + add_filter('register_post_type_args', [$this, 'modifyPostType'], 10, 2); - // 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); + return; } + + // Register the new PostType to WordPress. + 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(); + return array_replace_recursive($args, $this->generateOptions()); + } - return array_replace_recursive($args, $options); + /** + * Generate the options for the PostType. + * + * @return array + */ + public function generateOptions() + { + $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()); } /** - * Register Taxonomies to the PostType + * Modify the PostType filters. + * + * @param string $posttype * @return void */ - public function registerTaxonomies() + public function modifyFilters($posttype) { - if (empty($this->posttype->taxonomies)) { + if ($posttype !== $this->posttype->name()) { return; } - foreach ($this->posttype->taxonomies as $taxonomy) { - register_taxonomy_for_object_type($taxonomy, $this->posttype->name); + 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 and display filters on the admin edit screen - * @param string $posttype The current screen post type - * @return void + * Modify the PostType columns. + * + * @param array $columns + * @return array */ - public function modifyFilters($posttype) + public function modifyColumns(array $columns) { - // 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; - } + foreach ($this->columns->getColumns() as $key => $label) { + $columns[$key] = $label; + } - // If the taxonomy is not registered to the post type, continue. - if (!is_object_in_taxonomy($this->posttype->name, $taxonomy)) { - continue; - } + if ($remove = $this->columns->getRemoved()) { + $columns = array_diff_key($columns, array_flip($remove)); + } - // get the taxonomy object - $tax = get_taxonomy($taxonomy); + if ($only = $this->columns->getOnly()) { + $columns = array_intersect_key($columns, array_flip($only)); + } - // start the html for the filter dropdown - $selected = null; + foreach ($this->columns->getPositions() as $key => $position) { + [$direction, $reference] = $position; + + if (!isset($direction) || !isset($reference)) { + continue; + } - if (isset($_GET[$taxonomy])) { - $selected = sanitize_title($_GET[$taxonomy]); + $new = []; + + foreach ($columns as $k => $label) { + if ('before' === $direction && $k === $reference) { + $new[$key] = $columns[$key]; } - $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); + $new[$k] = $label; + + if ('after' === $direction && $k === $reference) { + $new[$key] = $columns[$key]; + } } + + $columns = $new; } - } - /** - * Modify the columns for the PostType - * @param array $columns Default WordPress columns - * @return array The modified columns - */ - public function modifyColumns($columns) - { - return $this->posttype->columns->modifyColumns($columns); + return $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]); + $callback = $this->columns->getPopulateCallback($column); + + if ($callback) { + call_user_func_array($callback, [$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); - } + $sortable = $this->columns->getSortableColumns(); - return $columns; + return array_merge($columns, $sortable); } /** - * 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); - - // 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'; - } + $column = $query->get('orderby'); + $callback = $this->columns->getSortCallback($column); - // set the custom order - $query->set('meta_key', $meta_key); - $query->set('orderby', $meta_value); + if ($callback) { + call_user_func_array($callback, [$query]); } } } diff --git a/src/Registrars/TaxonomyRegistrar.php b/src/Registrars/TaxonomyRegistrar.php index d6da048..ca1bd46 100644 --- a/src/Registrars/TaxonomyRegistrar.php +++ b/src/Registrars/TaxonomyRegistrar.php @@ -2,144 +2,203 @@ namespace PostTypes\Registrars; -use PostTypes\Taxonomy; +use PostTypes\Contracts\TaxonomyContract; +use PostTypes\Columns; class TaxonomyRegistrar { - protected $taxonomy; + /** + * Taxonomy to register. + * + * @var TaxonomyContract + */ + private $taxonomy; - public function __construct(Taxonomy $taxonomy) + /** + * Taxonomy Columns. + * + * @var Columns + */ + private $columns; + + /** + * Constructor. + * + * @param TaxonomyContract $taxonomy + */ + public function __construct(TaxonomyContract $taxonomy) { $this->taxonomy = $taxonomy; } + /** + * Register the Taxonomy to WordPress. + * + * @return void + */ public function register() { - // Get the Taxonomy name. - $name = $this->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); + add_action('init', [$this, 'createcolumns'], 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']); + // Handle Taxonomy columns. + add_filter('manage_edit-' . $name . '_columns', [$this, 'modifyColumns'], 10, 1); + 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); - // 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']); + // Register custom hooks. + $this->taxonomy->hooks(); + } - // run action that sorts columns on request - add_action('parse_term_query', [$this, 'sortSortableColumns']); - } + /** + * Create Columns. + * + * @return void + */ + public function createColumns() + { + $this->columns = $this->taxonomy->columns(new Columns()); } /** - * 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(), [], $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); + 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; } /** - * 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] - ); - } + $callback = $this->columns->getPopulateCallback($column); - return $content; + if ($callback) { + call_user_func_array($callback, [$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 array_merge($columns, $this->columns->getSortableColumns()); } /** - * 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'; - } + $column = $query->query_vars['orderby']; + $callback = $this->columns->getSortCallback($column); - // set the sort order - $query->query_vars['orderby'] = $orderby; - $query->query_vars['meta_key'] = $meta_key; + if ($callback) { + call_user_func_array($callback, [$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/ColumBuilderTest.php b/tests/ColumBuilderTest.php new file mode 100644 index 0000000..daed557 --- /dev/null +++ b/tests/ColumBuilderTest.php @@ -0,0 +1,122 @@ +createMock(Columns::class); + + $columns->expects($this->once()) + ->method('label') + ->with('price', 'Price Label'); + + $builder = new ColumnBuilder($columns, 'price'); + + $result = $builder->label('Price Label'); + + $this->assertSame($builder, $result); + } + + public function test_position_sets_position_correctly() + { + $columns = $this->createMock(Columns::class); + + $columns->expects($this->once()) + ->method('position') + ->with('price', 'after', 'title'); + + $builder = new ColumnBuilder($columns, 'price'); + + $result = $builder->position('after', 'title'); + + $this->assertSame($builder, $result); + } + + public function test_after_sets_position_after_reference() + { + $columns = $this->createMock(Columns::class); + + $columns->expects($this->once()) + ->method('position') + ->with('price', 'after', 'title'); + + $builder = new ColumnBuilder($columns, 'price'); + + $result = $builder->after('title'); + + $this->assertSame($builder, $result); + } + + public function test_before_sets_position_before_reference() + { + $columns = $this->createMock(Columns::class); + + $columns->expects($this->once()) + ->method('position') + ->with('price', 'before', 'title'); + + $builder = new ColumnBuilder($columns, 'price'); + + $result = $builder->before('title'); + + $this->assertSame($builder, $result); + } + + public function test_populate_sets_populate_callback() + { + $callback = function () {}; + + $columns = $this->createMock(Columns::class); + + $columns->expects($this->once()) + ->method('populate') + ->with('price', $callback); + + $builder = new ColumnBuilder($columns, 'price'); + + $result = $builder->populate($callback); + + $this->assertSame($builder, $result); + } + + public function test_sort_sets_sort_callback() + { + $callback = function () {}; + + $columns = $this->createMock(Columns::class); + + $columns->expects($this->once()) + ->method('sort') + ->with('price', $callback); + + $builder = new ColumnBuilder($columns, 'price'); + + $result = $builder->sort($callback); + + $this->assertSame($builder, $result); + } + + public function test_builder_fluency_all_methods_chain() + { + $columns = $this->createMock(Columns::class); + + $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($columns, '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 new file mode 100644 index 0000000..bc86be8 --- /dev/null +++ b/tests/ColumnTest.php @@ -0,0 +1,22 @@ +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()); + $this->assertEquals(null, $stub->position()); + $this->assertEquals(null, $stub->sort()); + } +} diff --git a/tests/ColumnsTest.php b/tests/ColumnsTest.php index ce2b217..aa6ede4 100644 --- a/tests/ColumnsTest.php +++ b/tests/ColumnsTest.php @@ -2,261 +2,196 @@ use PHPUnit\Framework\TestCase; use PostTypes\Columns; +use PostTypes\ColumnBuilder; +use PostTypes\Contracts\ColumnContract; class ColumnsTest extends TestCase { - protected $columns; - - protected function setUp(): void + public function test_can_label_column() { - $this->columns = new Columns; - } + $columns = new Columns; - /** @test */ - public function canCreateColumns() - { - $this->assertInstanceOf(Columns::class, $this->columns); + $columns->label('column', 'Test Column'); + + $output = $columns->getColumns(); + + $this->assertArrayHasKey('column', $output); + $this->assertSame('Test Column', $output['column']); } - /** @test */ - public function canSetColumns() + public function test_can_add_column_with_column_class() { - $columns = [ - 'title' => 'Title', - 'date' => 'Date', - ]; + $stub = $this->createMock(ColumnContract::class); + + $stub->method('name')->willReturn('column'); + $stub->method('label')->willReturn('Column'); + $stub->method('position')->willReturn(['after', 'title']); + $stub->method('populate')->willReturn(function () {}); + $stub->method('sort')->willReturn(function () {}); + + $columns = new Columns; + $columns->column($stub); - $this->columns->set($columns); + $output = $columns->getColumns(); + $positions = $columns->getPositions(); + $populate = $columns->getPopulateCallback('column'); + $sortable = $columns->getSortCallback('column'); - $this->assertEquals($this->columns->items, $columns); + $this->assertArrayHasKey('column', $output); + $this->assertSame('Column', $output['column']); + + $this->assertArrayHasKey('column', $positions); + $this->assertSame(['after', 'title'], $positions['column']); + + $this->assertIsCallable($populate); + $this->assertIsCallable($sortable); } - /** @test */ - public function canAddColumnsWithArray() + public function test_add_returns_column_builder() { - $columns = [ - 'genre' => 'Genre', - ]; + $columns = new Columns; - $this->columns->add($columns); + $builder = $columns->add('new_column'); - $this->assertEquals($this->columns->add, $columns); + $this->assertInstanceOf(ColumnBuilder::class, $builder); } - /** @test */ - public function canAddColumnsWithArgs() + public function test_modify_returns_column_builder() { - $this->columns->add('genre', 'Genre'); - - // Auto generated label1 - $this->columns->add('price'); + $columns = new Columns; - $expected = [ - 'genre' => 'Genre', - 'price' => 'Price', - ]; + $builder = $columns->modify('existing'); - $this->assertEquals($this->columns->add, $expected); + $this->assertInstanceOf(ColumnBuilder::class, $builder); } - /** @test */ - public function canHideColumns() + public function test_can_set_column_populate_callback() { - $columns = [ - 'date' - ]; + $columns = new Columns; - $this->columns->hide($columns); + $callback = function () {}; + $columns->populate('column', $callback); - $this->assertEquals($this->columns->hide, $columns); + $this->assertSame($callback, $columns->getPopulateCallback('column')); } - /** @test */ - public function canPopulateColumns() + public function test_get_populate_callback_returns_null_for_missing_key() { - $callable = function($column, $post_id) { - echo $post_id; - }; + $columns = new Columns; - $this->columns->populate('post_id', $callable); - - $this->assertEquals($this->columns->populate['post_id'], $callable); + $this->assertNull($columns->getPopulateCallback('missing')); } - /** @test */ - public function canOrderColumns() + public function test_can_set_remove_column() { - $columns = [ - 'date' => 3, - 'genre' => 2 - ]; + $columns = new Columns; - $this->columns->order($columns); + $columns->remove(['column']); - $this->assertEquals($this->columns->positions, $columns); + $this->assertEquals(['column'], $columns->getRemoved()); } - /** @test */ - public function canSortColumns() + public function test_can_set_remove_columns_with_multiple_calls() { - $columns = [ - 'rating' => ['_rating', true] - ]; + $columns = new Columns; - $this->columns->sortable($columns); + $columns->remove(['column']); + $columns->remove(['column_2']); - $this->assertEquals($this->columns->sortable, $columns); + $this->assertEquals(['column', 'column_2'], $columns->getRemoved()); } - /** @test */ - public function usesSetColumnsOverDefaults() + public function test_can_set_only_columns() { - $defaults = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; + $columns = new Columns; + + $columns->only(['one']); + $columns->only(['two']); - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'date' => 'Date' - ]; + $this->assertEquals(['one', 'two'], $columns->getOnly()); + } - $this->columns->set($columns); + public function test_can_set_position_after() + { + $columns = new Columns; - $output = $this->columns->modifyColumns($defaults); + $columns->position('col', 'after', 'title'); - $this->assertEquals($output, $columns); + $this->assertSame(['after', 'title'], $columns->getPositions()['col']); } - /** @test */ - public function addsColumnsToDefaults() + public function test_can_set_position_before() { - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; + $columns = new Columns; - $this->columns->add(['genre' => 'Genres']); + $columns->position('col', 'before', 'date'); - $output = $this->columns->modifyColumns($columns); + $this->assertSame(['before', 'date'], $columns->getPositions()['col']); + } - $columns['genre'] = 'Genres'; + public function test_position_throws_exception_for_invalid_direction() + { + $this->expectException(InvalidArgumentException::class); - $this->assertEquals($output, $columns); + $columns = new Columns; + $columns->position('col', 'sideways', 'title'); } - /** @test */ - public function hideColumnsFromDefaults() + public function test_can_set_sortable_column() { - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; + $columns = new Columns; - $this->columns->hide('comments'); + $callback = function () {}; + $columns->sort('column', $callback); - $output = $this->columns->modifyColumns($columns); + $this->assertSame($callback, $columns->getSortCallback('column')); + } - unset($columns['comments']); + public function test_get_sortable_columns_returns_keys_mapped_to_keys() + { + $columns = new Columns; + + $columns->sort('a', function () {}); + $columns->sort('b', function () {}); - $this->assertEquals($output, $columns); + $this->assertSame(['a' => 'a', 'b' => 'b'], $columns->getSortableColumns()); } - /** @test */ - public function setOrderOfDefaultColumns() + public function test_get_sortable_callback_returns_null_for_missing_key() { - $columns = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; - - $this->columns->order([ - 'date' => 1, - 'title' => 3 - ]); - - $output = $this->columns->modifyColumns($columns); - - $expected = [ - 'date' => 'Date', - 'author' => 'Author', - 'title' => 'Title', - 'comments' => 'Comments', - ]; - - $this->assertEquals($output, $expected); + $columns = new Columns; + + $this->assertNull($columns->getSortCallback('missing')); } - /** @test */ - public function canModifyColumns() + public function test_populate_does_not_affect_sort_callbacks() { - $defaults = [ - 'title' => 'Title', - 'author' => 'Author', - 'comments' => 'Comments', - 'date' => 'Date' - ]; + $columns = new Columns; - $expected = [ - 'title' => 'Title', - 'genre' => 'Genre', - 'author' => 'Author', - 'date' => 'Date' - ]; + $columns->populate('col', function () {}); - $this->columns->hide('comments'); - - $this->columns->add(['genre' => 'Genre']); + $this->assertNull($columns->getSortCallback('col')); + } - $this->columns->order([ - 'genre' => 2, - ]); + public function test_sort_does_not_affect_populate_callbacks() + { + $columns = new Columns; - $output = $this->columns->modifyColumns($defaults); + $columns->sort('col', function () {}); - $this->assertEquals($output, $expected); + $this->assertNull($columns->getPopulateCallback('col')); } - /** @test */ - public function canIdentifySortableColumns() + public function test_add_does_not_overwrite_callback_data() { - $columns = [ - 'rating' => ['_rating', true], - 'price' => '_price', - 'sortable' => ['sortable'], - ]; - - $this->columns->sortable($columns); - - $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')); - } + $columns = new Columns; - /** @test */ - public function returnsCorrectSortableMetaKey() - { - $columns = [ - 'rating' => ['_rating', true], - 'price' => '_price', - 'column' => ['sortable'], - ]; - - $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'), ''); + $columns->populate('col', function () {}); + $columns->sort('col', function () {}); + $columns->add('col', 'Label'); + + // Callbacks remain unchanged + $this->assertIsCallable($columns->getPopulateCallback('col')); + $this->assertIsCallable($columns->getSortCallback('col')); } } diff --git a/tests/PostTypeTest.php b/tests/PostTypeTest.php index 2e3bd91..c0bbd4d 100644 --- a/tests/PostTypeTest.php +++ b/tests/PostTypeTest.php @@ -2,302 +2,24 @@ use PHPUnit\Framework\TestCase; use PostTypes\PostType; -use PostTypes\Columns; class PostTypeTest extends TestCase { - protected $books; - - protected function setUp(): void - { - // setup basic PostType - $this->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, []); + public function test_post_type_returns_defaults() + { + $stub = $this->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 e6a64bd..0ac3db1 100644 --- a/tests/Registrars/PostTypeRegistrarTest.php +++ b/tests/Registrars/PostTypeRegistrarTest.php @@ -1,24 +1,173 @@ 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); + } + + public function test_can_modify_columns() + { + $defaults = [ + 'cb' => '', + 'title' => 'Title', + 'author' => 'Author', + ]; + + $columns = new Columns; + $columns->label('date', 'Date'); + + $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->createColumns(); + $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') + ->willReturnCallback(function() {}); + + $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->createColumns(); + $registrar->populateColumns('column', 1); + } + + public function test_can_set_sortable_columns() + { + $columns = new Columns; + $columns->sort('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($posttype); + $registrar = new PostTypeRegistrar($stub); + $registrar->createColumns(); + $output = $registrar->setSortableColumns($sortable); - $options = $registrar->modifyPostType([ - 'public' => true, - ], 'post'); + $expected = [ + 'title' => 'title', + 'column' => 'column', + ]; - $this->assertEquals(false, $options['public']); + $this->assertEquals($expected, $output); } } diff --git a/tests/Registrars/TaxonomyRegistrarTest.php b/tests/Registrars/TaxonomyRegistrarTest.php index 3a2494b..697b0f7 100644 --- a/tests/Registrars/TaxonomyRegistrarTest.php +++ b/tests/Registrars/TaxonomyRegistrarTest.php @@ -1,17 +1,160 @@ getMockForAbstractClass(Taxonomy::class); + + $stub->expects($this->any()) + ->method('name') + ->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->label('popularity', 'Popularity'); + + $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->createColumns(); + $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') + ->willReturnCallback(function() {}); + + $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->createColumns(); + $registrar->populateColumns('', 'column', 1); + } + + public function test_can_set_sortable_columns() + { + $columns = new Columns; + $columns->sort('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); + $registrar->createColumns(); + $output = $registrar->setSortableColumns($sortable); + + $expected = [ + 'title' => 'title', + 'column' => 'column', + ]; + + $this->assertEquals($expected, $output); + } } diff --git a/tests/TaxonomyTest.php b/tests/TaxonomyTest.php index 2883c19..a8049ec 100644 --- a/tests/TaxonomyTest.php +++ b/tests/TaxonomyTest.php @@ -2,166 +2,21 @@ use PHPUnit\Framework\TestCase; use PostTypes\Taxonomy; -use PostTypes\Columns; class TaxonomyTest extends TestCase { - protected $genres; - - protected function setUp(): void - { - $this->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() + public function test_taxonomy_returns_defaults() { - $names = [ - 'name' => 'genre', - 'singular' => 'Single Genre', - 'plural' => 'Multiple Genres', - 'slug' => 'slug-genres', - ]; - - $this->genres->names($names); + $stub = $this->getMockForAbstractClass(Taxonomy::class); - $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(); + $stub->expects($this->any()) + ->method('name') + ->will($this->returnValue('genre')); - // 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()); + $this->assertEquals('genre', $stub->slug()); + $this->assertEquals([], $stub->labels()); + $this->assertEquals([], $stub->options()); + $this->assertEquals([], $stub->posttypes()); + $this->assertEquals(null, $stub->hooks()); } }