row_actions[ $action->id() ] = $action; } /** * Offer a column on a list screen. * * @param Column $column Declared column. * @return void */ public function register_column( Column $column ) { $this->columns[ $column->id() ] = $column; } /** * Wait for a screen before hooking anything. * * Nothing is registered at boot. The real hooks go on in register_for_screen(), once the * screen tells us its post type — so a site with these features enabled still carries zero * list-table hooks on every admin screen that is not a post list. * * @return void */ public function boot() { if ( ! is_admin() ) { return; } add_action( 'current_screen', array( $this, 'register_for_screen' ) ); } /** * Register hooks for one list screen, if anything was declared for its post type. * * @param \WP_Screen $screen Current screen. * @return void */ public function register_for_screen( $screen ) { if ( ! $screen instanceof \WP_Screen ) { return; } // Three list screens, three different hook names for the same idea. The media library // is 'upload', everything else is 'edit'. if ( ! in_array( $screen->base, array( 'edit', 'upload' ), true ) ) { return; } $post_type = 'upload' === $screen->base ? 'attachment' : $screen->post_type; if ( '' === $post_type ) { return; } if ( ! empty( $this->applicable( $this->row_actions, $post_type ) ) ) { add_filter( $this->row_action_hook( $screen, $post_type ), array( $this, 'filter_row_actions' ), 10, 2 ); } if ( 'upload' === $screen->base ) { // The media library has no column hooks worth supporting yet, and its custom-column // action is named differently again. Row actions only. return; } $columns = $this->applicable( $this->columns, $post_type ); if ( empty( $columns ) ) { return; } add_filter( "manage_{$post_type}_posts_columns", array( $this, 'filter_columns' ) ); add_action( "manage_{$post_type}_posts_custom_column", array( $this, 'render_column' ), 10, 2 ); foreach ( $columns as $column ) { if ( $column->is_sortable() ) { add_filter( "manage_edit-{$post_type}_sortable_columns", array( $this, 'filter_sortable_columns' ) ); break; } } } /** * Which hook renders row actions on this screen. * * Three names for one concept, and picking the wrong one loses every action silently: * hierarchical types render through page_row_actions, the media library through * media_row_actions, and everything else through post_row_actions. * * @param \WP_Screen $screen Current screen. * @param string $post_type Post type being listed. * @return string */ private function row_action_hook( \WP_Screen $screen, $post_type ) { if ( 'upload' === $screen->base ) { return 'media_row_actions'; } return is_post_type_hierarchical( $post_type ) ? 'page_row_actions' : 'post_row_actions'; } /** * Append our actions to a row. * * @param array $actions Existing row actions. * @param \WP_Post $post Post being rendered. * @return array */ public function filter_row_actions( $actions, $post ) { foreach ( $this->row_actions as $action ) { if ( ! $action->is_available_for( $post ) ) { continue; } $url = $action->url_for( $post ); if ( '' === $url ) { continue; } $actions[ $action->id() ] = sprintf( '%s', esc_url( $url ), esc_html( $action->label() ) ); } return $actions; } /** * Insert our columns into the heading row. * * @param array $columns Existing columns, key => heading. * @return array */ public function filter_columns( $columns ) { $post_type = $this->current_post_type(); foreach ( $this->applicable( $this->columns, $post_type ) as $column ) { $columns = $this->insert_column( $columns, $column ); } return $columns; } /** * Place one column, honouring its requested position. * * @param array $columns Existing columns. * @param Column $column Column to place. * @return array */ private function insert_column( array $columns, Column $column ) { $before = $column->before(); if ( null === $before || ! array_key_exists( $before, $columns ) ) { $columns[ $column->id() ] = $column->label(); return $columns; } $placed = array(); foreach ( $columns as $key => $heading ) { if ( $key === $before ) { $placed[ $column->id() ] = $column->label(); } $placed[ $key ] = $heading; } return $placed; } /** * Render one cell. * * Core passes the column key and a post id, and expects output to be echoed. The Column is * responsible for escaping — it returns HTML by contract. * * @param string $column_key Column being rendered. * @param int $post_id Post id. * @return void */ public function render_column( $column_key, $post_id ) { if ( ! isset( $this->columns[ $column_key ] ) ) { return; } $post = get_post( $post_id ); if ( ! $post instanceof \WP_Post ) { return; } $column = $this->columns[ $column_key ]; if ( ! $column->applies_to( $post->post_type ) ) { return; } // Escaped here as well as by the column: a render callback is a feature's code, and output // is escaped at the point it is printed, whatever the callback promises. echo wp_kses_post( $column->render_for( $post ) ); } /** * Declare which of our columns are sortable. * * @param array $sortable Existing sortable columns. * @return array */ public function filter_sortable_columns( $sortable ) { $post_type = $this->current_post_type(); foreach ( $this->applicable( $this->columns, $post_type ) as $column ) { if ( $column->is_sortable() ) { $sortable[ $column->id() ] = $column->orderby(); } } return $sortable; } /** * Filter a set of declarations down to those applying to a post type. * * @param RowAction[]|Column[] $items Declarations. * @param string $post_type Post type slug. * @return array */ private function applicable( array $items, $post_type ) { $found = array(); foreach ( $items as $key => $item ) { if ( $item->applies_to( $post_type ) ) { $found[ $key ] = $item; } } return $found; } /** * Post type of the screen being rendered. * * @return string */ private function current_post_type() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; return ( $screen instanceof \WP_Screen && '' !== $screen->post_type ) ? $screen->post_type : 'post'; } }