Skip to content

Add PHP Scaffold for CPTs and Taxonomies #175

@darylldoyle

Description

@darylldoyle

Is your enhancement related to a problem? Please describe.

All new 10up projects are based on this repo, but there is very little PHP scaffolding in place. This means that each different project ends up with a slightly different implementation for post types, taxonomies, REST endpoints etc.

This has two effects:

  • Slows down the initial setup of a project, as the tech lead has to put these scaffolds in place.
  • Increases the cognitive load for engineers that are switching between projects, as they have to learn the differences in setups.

Designs

Since the auto-initialisation of modules was merged in #158, it's allowed us to create much more modular scaffolds, with no requirement for factory classes etc.

I propose introducing abstract classes that contain all the base information needed to register a CPT/Taxonomy, which can then be extended to implement project-specific ones.

For example, the following abstract post type would then allow us to easily implement further ones:

<?php
/**
 * AbstractPostType
 *
 * @package TenUpPlugin
 */

namespace TenUpPlugin\PostTypes;

use TenUpPlugin\Module;

/**
 * Abstract class for post types.
 */
abstract class AbstractPostType extends Module {

	/**
	 * Get the post type name.
	 *
	 * @return string
	 */
	abstract public function get_name();

	/**
	 * Get the singular post type label.
	 *
	 * @return string
	 */
	abstract public function get_singular_label();

	/**
	 * Get the plural post type label.
	 *
	 * @return string
	 */
	abstract public function get_plural_label();

	/**
	 * Default post type supported feature names.
	 *
	 * @return array
	 */
	public function get_editor_supports() {
		$supports = [
			'title',
			'editor',
			'author',
			'thumbnail',
			'excerpt',
			'revisions',
		];

		return $supports;
	}

	/**
	 * Get the options for the post type.
	 *
	 * @return array
	 */
	public function get_options() {
		$options = [
			'labels'            => $this->get_labels(),
			'public'            => true,
			'has_archive'       => true,
			'show_ui'           => true,
			'show_in_menu'      => true,
			'show_in_nav_menus' => false,
			'show_in_rest'      => true,
			'supports'          => $this->get_editor_supports(),
		];

		return $options;
	}

	/**
	 * Get the labels for the post type.
	 *
	 * @return array
	 */
	public function get_labels() {
		$plural_label   = $this->get_plural_label();
		$singular_label = $this->get_singular_label();

		// phpcs:disable -- ignoring template strings without translators placeholder since this is dynamic
		$labels = array(
			'name'                     => $plural_label,
			// Already translated via get_plural_label().
			'singular_name'            => $singular_label,
			// Already translated via get_singular_label().
			'add_new_item'             => sprintf( __( 'Add New %s', 'tenup-plugin' ), $singular_label ),
			'edit_item'                => sprintf( __( 'Edit %s', 'tenup-plugin' ), $singular_label ),
			'new_item'                 => sprintf( __( 'New %s', 'tenup-plugin' ), $singular_label ),
			'view_item'                => sprintf( __( 'View %s', 'tenup-plugin' ), $singular_label ),
			'view_items'               => sprintf( __( 'View %s', 'tenup-plugin' ), $plural_label ),
			'search_items'             => sprintf( __( 'Search %s', 'tenup-plugin' ), $plural_label ),
			'not_found'                => sprintf( __( 'No %s found.', 'tenup-plugin' ), strtolower( $plural_label ) ),
			'not_found_in_trash'       => sprintf( __( 'No %s found in Trash.', 'tenup-plugin' ), strtolower( $plural_label ) ),
			'parent_item_colon'        => sprintf( __( 'Parent %s:', 'tenup-plugin' ), $plural_label ),
			'all_items'                => sprintf( __( 'All %s', 'tenup-plugin' ), $plural_label ),
			'archives'                 => sprintf( __( '%s Archives', 'tenup-plugin' ), $singular_label ),
			'attributes'               => sprintf( __( '%s Attributes', 'tenup-plugin' ), $singular_label ),
			'insert_into_item'         => sprintf( __( 'Insert into %s', 'tenup-plugin' ), strtolower( $singular_label ) ),
			'uploaded_to_this_item'    => sprintf( __( 'Uploaded to this %s', 'tenup-plugin' ), strtolower( $singular_label ) ),
			'filter_items_list'        => sprintf( __( 'Filter %s list', 'tenup-plugin' ), strtolower( $plural_label ) ),
			'items_list_navigation'    => sprintf( __( '%s list navigation', 'tenup-plugin' ), $plural_label ),
			'items_list'               => sprintf( __( '%s list', 'tenup-plugin' ), $plural_label ),
			'item_published'           => sprintf( __( '%s published.', 'tenup-plugin' ), $singular_label ),
			'item_published_privately' => sprintf( __( '%s published privately.', 'tenup-plugin' ), $singular_label ),
			'item_reverted_to_draft'   => sprintf( __( '%s reverted to draft.', 'tenup-plugin' ), $singular_label ),
			'item_scheduled'           => sprintf( __( '%s scheduled.', 'tenup-plugin' ), $singular_label ),
			'item_updated'             => sprintf( __( '%s updated.', 'tenup-plugin' ), $singular_label ),
			'menu_name'                => $plural_label,
			'name_admin_bar'           => $singular_label,
		);
		// phpcs:enable

		return $labels;
	}

	/**
	 * Registers a post type and associates its taxonomies.
	 *
	 * @uses $this->get_name() to get the post's type name.
	 * @return Bool Whether this theme has supports for this post type.
	 */
	public function register() {
		$this->register_post_type();
		$this->register_taxonomies();

		$this->after_register();

		return true;
	}

	/**
	 * Registers the current post type with WordPress.
	 *
	 * @return void
	 */
	public function register_post_type() {
		register_post_type(
			$this->get_name(),
			$this->get_options()
		);
	}

	/**
	 * Registers the taxonomies declared with the current post type.
	 *
	 * @return void
	 */
	public function register_taxonomies() {
		$taxonomies = $this->get_supported_taxonomies();

		$object_type = $this->get_name();

		if ( ! empty( $taxonomies ) ) {
			foreach ( $taxonomies as $taxonomy ) {
				register_taxonomy_for_object_type(
					$taxonomy,
					$object_type
				);
			}
		}
	}

	/**
	 * Returns the default supported taxonomies. The subclass should declare the
	 * Taxonomies that it supports here if required.
	 *
	 * @return array
	 */
	public function get_supported_taxonomies() {
		return [];
	}

	/**
	 * Run any code after the post type has been registered.
	 *
	 * @return void
	 */
	public function after_register() {
		// Do nothing.
	}

}

To implement a project-specific CPT, we'd then do something like:

<?php
/**
 * Demo Post Type
 *
 * @package TenUpPlugin
 */

namespace TenUpPlugin\PostTypes;

/**
 * Demo post type.
 */
class Demo extends AbstractPostType {

	/**
	 * Get the post type name.
	 *
	 * @return string
	 */
	public function get_name() {
		return 'tenup-demo';
	}

	/**
	 * Get the singular post type label.
	 *
	 * @return string
	 */
	public function get_singular_label() {
		return esc_html__( 'Demo', 'tenup-plugin' );
	}

	/**
	 * Get the plural post type label.
	 *
	 * @return string
	 */
	public function get_plural_label() {
		return esc_html__( 'Demos', 'tenup-plugin' );
	}

	/**
	 * Can the class be registered?
	 *
	 * @return bool
	 */
	public function can_register() {
		return true;
	}

	/**
	 * Supported post type features.
	 *
	 * @return array
	 */
	public function get_editor_supports() {
		return [
			'title',
			'editor',
			'thumbnail',
			'page-attributes',
			'excerpt',
			'revisions',
			'custom-fields',
		];
	}

	/**
	 * Get the options for the post type.
	 *
	 * @return array
	 */
	public function get_options() {
		return array_merge(
			parent::get_options(),
			[
				'hierarchical' => false,
				'rewrite'      => [
					'slug' => 'demo',
				],
			]
		);
	}

	/**
	 * Returns the default supported taxonomies. The subclass should declare the
	 * Taxonomies that it supports here if required.
	 *
	 * @return array
	 */
	public function get_supported_taxonomies() {
		return [
			'tenup-tax-demo',
		];
	}
}

Due to the fact that we have auto-initializing modules, the engineer would only need to create the sub-class and fill it out. There's no need to add anything to a factory, meaning less merge conflicts.

We can also do something very similar for taxonomies.

This solution would provide engineers with:

  • A standardised, extendable way to register post types
  • A pre-existing scaffold within the repo
  • Loss cognitive overhead when switching between projects.

It also gives us a nice lead-in to creating a tool to help scaffold these items quickly, much like has been proposed in #89

Describe alternatives you've considered

Darshan has also submitted #104. I don't feel like this is an alternative to that approach, but it could be complimentary to it by proving a start for that modular scaffold.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions