CMB2: Repeatable Groups

Some data migration of individual post metas into grouped post meta. Part of a continuing saga in how Sara Lance wants to drive me insane.

This is something that the plugin does out of the box, but my reason for doing it was a little odd.


Originally, I had a set of TV characters as a custom post type and each one had their own TV show. Since the TV shows are a second post type, the data was saved as a number and that number was used to generate data on the show pages. Look for everyone who has a TV show value of the same ID as the post ID. Yay!

The problem with it was spinoffs and crossovers. As time went on, certain characters began to appear on other shows. And it only got worse, until at length there were 30 characters on more than one show, and the number was only growing.

The quick fix was to make the shows value a repeatable field in CMB2, where I could add multiple shows. Done and done. But then we reached critical mass with how we were handling character roles. Was the character a main, a recurring, or a guest?

Shows and Roles

Breaking down the problem to it’s most simple, we have one data set:

  • Show (stored as an ID in an array)
  • Role Type (stored as plain text)

Instead of saving it as a data set together, the shows were one field (an array, as I mentioned) and the role types were another (a text field).

In order to make this work, I would have to:

  1. Create a field ‘group’ in CMB2 that stored both show and role as related to that show
  2. Make that group repeatable for characters on multiple shows
  3. Migrate the data

Data Migration

There are a lot of ways around this. I ended up with going for the super simple route. I exported two CSVs from my database: one of the shows and one of the role types. Each one had the Post ID associated with it, so I opened those up in a spreadsheet app and combined them, for all cases where the Post ID was the same.

This gave me a new table that looked like this: 123, 456, regular

More or less. The ones where shows were arrays looked like, obviously, arrays. I then converted that into a file with 1500 lines that looked like this:

wp post meta add 6957 character_tvshow_group '[{"show":"6951","type":"regular"},{"show":"7009","type":"regular"}]' --format=json

I could have done it differently, grabbing a file with the data and parsing it on the fly, but I like to look at my 1500 lines and make sure I don’t have weird extra quotes lying around.

Once that was done, I ran the file, having it execute every line one at a time. It took about one episode of House Hunters: International.

The CMB2 Code

In case you’re wondering the code to do this in CMB2 looks like this:

		// Field Group: Character Show information
		// Made repeatable since each show might have a separate role. Yikes...
		$group_shows = $cmb2->add_field( array(
			'id'          => $prefix . 'show_group',
			'type'        => 'group',
			'repeatable'  => true,
			'options'     => array(
				'group_title'   => 'Show #{#}',
				'add_button'    => 'Add Another Show',
				'remove_button' => 'Remove Show',
				'sortable' => true,
		) );
		// Field: Show Name
		$cmb2->add_group_field( $group_shows, array(
			'name'             => 'TV Show',
			'id'               => 'show',
			'type'             => 'select',
			'show_option_none' => true,
			'default'          => 'custom',
			'options_cb'       => array( $this, 'cmb2_get_shows_options'),
		) );
		// Field: Character Type
		$cmb2->add_group_field( $group_shows, array(
			'name'             => 'Character Type',
			'id'               => 'type',
			'type'             => 'select',
			'show_option_none' => true,
			'default'          => 'custom',
			'options'          => $this->character_roles,
		) );

You’ll notice the options are a bit extra custom.

Get Shows

This is done in two parts:

	public function Sitename_get_post_options( $query_args ) {
	    $args = wp_parse_args( $query_args, array(
	        'post_type'   => 'post',
	        'numberposts' => wp_count_posts( 'post' )->publish,
	        'post_status' => array('publish'),
	    ) );

	    $posts = get_posts( $args );

	    $post_options = array();
	    if ( $posts ) {
	        foreach ( $posts as $post ) {
	          $post_options[ $post->ID ] = $post->post_title;

	    return $post_options;

	public function cmb2_get_shows_options() {
		return SiteName_get_post_options( array(
				'post_type'   => 'post_type_shows',
				'numberposts' => wp_count_posts( 'post_type_shows' )->publish,
				'post_status' => array('publish', 'pending', 'draft', 'future'),
			) );

The reason we search for all shows, from draft to future, is that sometimes we like to schedule updates.

Character Roles

		$this->character_roles = array(
			'regular'   => 'Regular/Main Character',
			'recurring'	=> 'Recurring Character',
			'guest'	 	=> 'Guest Character',

One comment

  1. The evolving nature of your data is similar to what I am working through with a small site that records the changing stores in a local shopping mall.

    Initially I stored the store’s opening and closing dates as custom fields and the unit number as a regular tag. Some stores are temporary (e.g. only open around Christmas) or have opened in multiple units in the mall. I am having to redo everything – I am going with Advanced Custom Fields Pro and its Repeater field.

    Querying the data efficiently will be fun. But it’s an interesting challenge

Comments are closed.

%d bloggers like this: