20.4.5.1. Eigenen Endpunkt-Controller für benutzerdefinierte Inhaltstypen

Gehen wir nochmal zurück zum Beispiel mit dem benutzerdefinierten Inhaltstyp book und nehmen an, wir haben über ein Buch geschrieben und den Artikel veröffentlicht. Rufen wir nun den Artikel über die REST-API auf:

GET http://test.local/wp-json/wp/v2/books/5?_embed

Erhalten wir folgende Antwort (Auszug):

{
  "id": 5,
  "date": "2018-08-29T12:13:51",
  "date_gmt": "2018-08-29T12:13:51",
  "guid": {
    "rendered": "http://test.local/?post_type=book&p=5"
  },
  "modified": "2018-08-29T12:13:51",
  "modified_gmt": "2018-08-29T12:13:51",
  "slug": "origin",
  "status": "publish",
  "type": "book",
  "link": "http://test.local/book/origin/",
  "title": {
    "rendered": "Origin"
  },
  "content": {
    "rendered": "...",
    "protected": false
  },
  "excerpt": {
    "rendered": "...",
    "protected": false
  },
  "author": 1,
  "featured_media": 0,
  "comment_status": "open",
  "ping_status": "closed",
  "template": "",
  "_links": {
    "self": [
      {
        "href": "http://test.local/wp-json/wp/v2/books/5"
      }
    ],
    "collection": [
      {
        "href": "http://test.local/wp-json/wp/v2/books"
      }
    ],
    "about": [
      {
        "href": "http://test.local/wp-json/wp/v2/types/book"
      }
    ],
    "author": [
      {
        "embeddable": true,
        "href": "http://test.local/wp-json/wp/v2/users/1"
      }
    ],
    "replies": [
      {
        "embeddable": true,
        "href": "http://test.local/wp-json/wp/v2/comments?post=5"
      }
    ],
    "wp:attachment": [
      {
        "href": "http://test.local/wp-json/wp/v2/media?parent=5"
      }
    ],
    "curies": [
      {
        "name": "wp",
        "href": "https://api.w.org/{rel}",
        "templated": true
      }
    ]
  },
  "_embedded": {
    "author": [
      {
        "id": 1,
        "name": "florian",
        "url": "",
        "description": "",
        "link": "http://test.local/author/florian/",
        "slug": "florian",
        "avatar_urls": {
          "24": "http://0.gravatar.com/avatar/c2b06ae950033b392998ada50767b50e?s=24&d=mm&r=g",
          "48": "http://0.gravatar.com/avatar/c2b06ae950033b392998ada50767b50e?s=48&d=mm&r=g",
          "96": "http://0.gravatar.com/avatar/c2b06ae950033b392998ada50767b50e?s=96&d=mm&r=g"
        },
        "_links": {
          "self": [
            {
              "href": "http://test.local/wp-json/wp/v2/users/1"
            }
          ],
          "collection": [
            {
              "href": "http://test.local/wp-json/wp/v2/users"
            }
          ]
        }
      }
    ]
  }
}

Zur Erinnerung: über den Parameter _embed erhalten wir auch die zugehörigen Daten für die embeddable-Links. In diesem Fall zum Autor.

Wenn wir einen genauen Blick auf die Antwort werfen, merken wir, dass der Autor nicht der Autor des Buches ist, über den wir geschrieben haben. Stattdessen liefert uns WordPress hier den Autor des benutzerdefinierten Artikels. Das ist ein WordPress Benutzer. Das kann irreführend sein und deshalb wollen wir das nun ändern. Das gelingt am besten mit einem eigenen Endpunkt-Controller.

Vorher werfen wir aber noch einen Blick auf die Klasse WP_REST_Controller. Von dieser wollen wir schließlich ableiten da sie extra für den Zweck entwickelt wurde, einheitliche Rückgabewerte zu schaffen. Folgende Methoden existieren:

You’re not allowed to see this content. Please log in first.

Datei: books/books.php

<?php
/*
Plugin Name: Custom Books
Description: Example of how a custom post type is used in the REST API.
*/

# Register custom post types
include __DIR__ . '/inc/types.php';
?>

Gesamtes Beispiel bei Github

Verknüpft wird ein Buch jeweils mit einem Autor über einen Postmeta-Wert.

Datei: books/inc/types.php

<?php

include __DIR__ . '/types/books.php';
include __DIR__ . '/types/book-authors.php';

add_action( 'init', 'mm_register_post_types' );

function mm_register_post_types() {

	mm_register_book_post_type();
	mm_register_author_post_type();

	register_post_meta(
		'book',
		'book_author',
		[
			'single'       => true,
			'show_in_rest' => false,
			'description'  => __( 'The ID of the author of the book.', 'mm-rest-example' ),
			'type'         => 'integer',
		]
	);

}
?>

Als nächstes wollen wir den benutzerdefinierten Artikeltyp book mitteilen, dass wir einen eigenen Endpunkt-Controller nutzen wollen:

Datei: books/inc/types/books.php (Auszug)

<?php

/**
 * Registers custom post type 'book'.
 */
function mm_register_book_post_type() {
	...

	$args = array(
		'labels'                => $labels,
		'public'                => true,
		...
		'show_in_rest'          => true,
		'rest_base'             => 'books',
		'rest_controller_class' => 'MM_REST_Books_Controller',
	);

	register_post_type( 'book', $args );
}
?>

Natürlich fehlt dann nur noch die Endpunkt-Controller-Klasse selbst:

Datei: books/inc/rest/controllers/books.php

<?php

/**
 * Class MM_REST_Books_Controller.
 */
class MM_REST_Books_Controller extends WP_REST_Posts_Controller {

	/**
	 * MM_REST_Books_Controller constructor.
	 *
	 * @param string $post_type
	 */
	public function __construct( $post_type ) {
		parent::__construct( $post_type );
	}
}
?>

Zwischenstand des Plugins auf Github ansehen

Als nächstes ändern wir die Beschreibung des Autors in den Schema-Daten:

Datei: books/inc/rest/controllers/books.php

<?php

/**
 * Class MM_REST_Books_Controller.
 */
class MM_REST_Books_Controller extends WP_REST_Posts_Controller {

	/**
	 * MM_REST_Books_Controller constructor.
	 *
	 * @param string $post_type
	 */
	public function __construct( $post_type ) {
		parent::__construct( $post_type );
	}

	/**
	 * Changes author description.
	 * 
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();

		if ( isset( $schema['properties']['author'] ) ) {
			$schema['properties']['author']['description'] = __( 'The ID of the author of the book.', 'mm-rest-example' );
		}


		return $schema;
	}
}
?>

Nun könnten wir die Methode prepare_item_for_response() komplett überschreiben. Aber WordPress hält für uns bereits einen entsprechenden Filter-Hook bereit, den wir hier nutzen können: rest_prepare_{$this->post_type}. Diesen wollen wir verwenden um die Antwort entsprechend zu manipulieren.

Wir registrieren den Filter im Konstruktor der Klasse und ändern die ID des Autors zu der ID des Buchautors ab:

Datei: books/inc/rest/controllers/books.php

<?php

/**
 * Class MM_REST_Books_Controller.
 */
class MM_REST_Books_Controller extends WP_REST_Posts_Controller {

	/**
	 * MM_REST_Books_Controller constructor.
	 *
	 * @param string $post_type
	 */
	public function __construct( $post_type ) {
		parent::__construct( $post_type );
		add_filter( 'rest_prepare_book', [ $this, 'manipulate_item' ], 10, 2 );
	}

	/**
	 * Changes author description.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();

		if ( isset( $schema['properties']['author'] ) ) {
			$schema['properties']['author']['description'] = __( 'The ID of the author of the book.', 'mm-rest-example' );
		}


		return $schema;
	}

	/**
	 * Manipulates the response to match our purposes.
	 *
	 * @param WP_REST_Response $response
	 * @param WP_Post          $post
	 *
	 * @return WP_REST_Response
	 */
	public function manipulate_item( $response, $post ) {

		$data = $response->get_data();

		$book_author_id = get_post_meta( $post->ID, 'book_author', true );
		$data['author'] = $book_author_id;

		$response->set_data( $data );

		return $response;
	}
}
?>

Was nun noch fehlt ist die Korrektur der CURIE-Links. Denn hier zeigt der Autor nach wie vor auf einen Benutzernamen. Stattdessen möchten wir aber, dass er auf einen book-author-Artikel zeigt.

Dieses mal bietet WordPress keinen Hook an. Deswegen müssen wir die Methode prepare_links() überschreiben.

Datei: books/inc/rest/controllers/books.php

<?php

/**
 * Class MM_REST_Books_Controller.
 */
class MM_REST_Books_Controller extends WP_REST_Posts_Controller {

	...

	/**
	 * Change CURIE links.
	 *
	 * @param WP_Post $post
	 *
	 * @return array
	 */
	protected function prepare_links( $post ) {
		$links = parent::prepare_links( $post );

		$book_author_id = get_post_meta( $post->ID, 'book_author', true );

		$links['author'] = array(
			'href'       => rest_url( 'wp/v2/book-authors/' . $book_author_id ),
			'embeddable' => true,
		);

		return $links;
	}
}

Gesamtes Plugin bei Github ansehen

Beispielhaft legen wir uns nun in WordPress ein book-Artikel sowie einen book-author-Artikel an. Über „Ansicht anpassen“ können Sie sich die „Benutzerdefinierte Felder“-Metabox anzeigen lassen (siehe Bild unten), in der Sie dann die ID des book-author-Artikels angeben:

WordPress Dashboard mit Hinweis zum Anpassen der Ansicht
Über „Ansicht anpassen“ erhält man die Metabox für die „Benutzerdefinierten Felder“
WordPress Dashboard mit Metabox für Benutzerdefinierte Felder
Eingabe der ID des Autors vom Buch

Ein Aufruf der REST-API gibt jetzt die gewünschten Informationen zurück und ersetzt den WordPress-Autor-Benutzer durch den entsprechenden book-author-Artikel:

GET http://test.local/wp-json/wp/v2/books/5?_embed
{
  "id": 5,
  "date": "2018-08-29T12:13:51",
  "date_gmt": "2018-08-29T12:13:51",
  "guid": {
    "rendered": "http://test.local/?post_type=book&p=5"
  },
  "modified": "2018-08-29T14:00:18",
  "modified_gmt": "2018-08-29T14:00:18",
  "slug": "origin",
  "status": "publish",
  "type": "book",
  "link": "http://test.local/book/origin/",
  "title": {
    "rendered": "Origin"
  },
  "content": {
    "rendered": "...",
    "protected": false
  },
  "excerpt": {
    "rendered": "...",
    "protected": false
  },
  "author": "6",
  "featured_media": 0,
  "comment_status": "open",
  "ping_status": "closed",
  "template": "",
  "meta": [],
  "_links": {
    "self": [
      {
        "href": "http://test.local/wp-json/wp/v2/books/5"
      }
    ],
    "collection": [
      {
        "href": "http://test.local/wp-json/wp/v2/books"
      }
    ],
    "about": [
      {
        "href": "http://test.local/wp-json/wp/v2/types/book"
      }
    ],
    "author": [
      {
        "embeddable": true,
        "href": "http://test.local/wp-json/wp/v2/book-authors/6"
      }
    ],
    "replies": [
      {
        "embeddable": true,
        "href": "http://test.local/wp-json/wp/v2/comments?post=5"
      }
    ],
    "wp:attachment": [
      {
        "href": "http://test.local/wp-json/wp/v2/media?parent=5"
      }
    ],
    "curies": [
      {
        "name": "wp",
        "href": "https://api.w.org/{rel}",
        "templated": true
      }
    ]
  },
  "_embedded": {
    "author": [
      {
        "id": 6,
        "date": "2018-08-29T13:36:13",
        "slug": "dan-brown",
        "type": "book-author",
        "link": "http://test.local/book-author/dan-brown/",
        "title": {
          "rendered": "Dan Brown"
        },
        "excerpt": {
          "rendered": "",
          "protected": false
        },
        "author": 1,
        "featured_media": 0,
        "_links": {
          "self": [
            {
              "href": "http://test.local/wp-json/wp/v2/book-authors/6"
            }
          ],
          "collection": [
            {
              "href": "http://test.local/wp-json/wp/v2/book-authors"
            }
          ],
          "about": [
            {
              "href": "http://test.local/wp-json/wp/v2/types/book-author"
            }
          ],
          "author": [
            {
              "embeddable": true,
              "href": "http://test.local/wp-json/wp/v2/users/1"
            }
          ],
          "replies": [
            {
              "embeddable": true,
              "href": "http://test.local/wp-json/wp/v2/comments?post=6"
            }
          ],
          "wp:attachment": [
            {
              "href": "http://test.local/wp-json/wp/v2/media?parent=6"
            }
          ],
          "curies": [
            {
              "name": "wp",
              "href": "https://api.w.org/{rel}",
              "templated": true
            }
          ]
        }
      }
    ]
  }
}

Natürlich sind wir hier noch lange nicht am Ende. Weitere Anpassungen wäre nötig um das Komplettpaket abzurunden. Das Datum könnte zum Beispiel ebenfalls ersetzt werden. Und zwar durch das Erscheinungsjahr des Buchs.

Schauen wir uns im nächsten Unterkapitel einen weiteren Controller an.