10up / wp-scaffold

10up WordPress project scaffold.
MIT License
204 stars 48 forks source link

Add PHP Scaffold for CPTs and Taxonomies #175

Closed darylldoyle closed 7 months ago

darylldoyle commented 1 year ago

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:

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:

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 https://github.com/10up/wp-scaffold/issues/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

fabiankaegy commented 9 months ago

From my perspective something like this would be very useful because pretty much every single project I've worked on needed multiple CPT's and every project does them slightly differently.

I am in favor of having the CPT's and anything that dictates the information architecture inside a plugin / plugins.