Show Events Based on Custom Fields

👋 This example only applies to sites using the updated view designs (V2), the new View architecture released in The Events Calendar 5.0.

In this article, we’re going to build a simple plugin to show a link, at the top of each calendar view, that allows users to either show all events or only show events that allow the walk-in purchase of tickets.

Before we start, it’s important to understand what we’re building from a user perspective: take a moment to look at the screenshots below to understand the simple user flow before delving into the code.

On the calendar’s list view, you can see the link above the search bar.

In the screenshot above, the user did not click the link yet.

When the link is clicked the view will refresh to show only events that allow walk-in purchases of tickets:

The plugin we’re developing will allow site administrators to set an event to allow walk-in ticket purchases by checking a box in the event editor:

Requirements

There aren’t many requirements here, but there are a few key things you’ll need to follow along:

  • A working WordPress development installation, one that you can mess with without compromising a live website.
  • A code editor to create and modify directories and files of your WordPress installation.
  • Some basic PHP and WordPress knowledge; we won’t delve deep into implementations and nerdy details, but we’re going to assume you have some experience working with WordPress templates.
  • The Events Calendar plugin installed and activated on the site.

For the sake of brevity, and to avoid burdening this article with too many technical tasks, we’ve taken some development “shortcuts” you should be aware of:

  • The plugin text is not localization-ready; if this was a plugin aimed at distribution, then all strings would use WordPress internationalization functions.
  • The plugin packs all of its code in a single file; this is to make it easier to copy and paste the example code. If this was a plugin aimed at distribution, then the code would be broken down into more focused files.
  • The code is commented. The code comments are by no means exhaustive, but please follow along with this tutorial for a more exhaustive explanation of the why’s and how’s.

Setting up a minimal test environment

To start understanding what we’re going to do, and run your own manual tests while playing around with the code, you need to set up the WordPress installation correctly:

  • Start by installing and activating the latest version of The Events Calendar plugin.
  • Create a new plugin as detailed in the plugin code section.
  • Activate the plugin.
  • Create a minimum of two events: one that allows walk-in purchases and another that does not.

Context and ORM

Before jumping into the code it’s worth spending some time defining the words and concepts you see presented here.

Context

The “Context” (capital “C”) represents the abstraction by which we gather information about the context (lowercase “c”) of the current request.

What’s part of a request context?

  • What calendar view (e.g. month, list, etc.) is the user looking at?
  • Is the user trying to filter the results by means of a search, a date, or other controls? And what are the values of those filters?
  • Are we processing a PHP initial state request (i.e. one fired when the user first visits a page on the site or refreshes the page), an AJAX request fired by our code, or a REST API request?

This list does not contain all the different sources and information bits we need to understand where the code is executing but it does provide an idea of how varied this information-gathering operation can be.

To answer these questions, we might need to call different functions or class methods to paint a picture:

  • WordPress functions
  • WordPress constants
  • WordPress global variables

Then we ought to dive a step deeper into The Events Calendar’s own functions, constants, and globals to get information specific to event posts.

Now, imagine all this “information gathering” is neatly packed and hidden away under a consistent API that would answer each question in the list above like this:

  • $view = tribe_context()->get( ‘request_view’ );
  • $date = tribe_context()->get( ‘event_date’, ‘today’ );
  • $doing_php_initial_state = tribe_context()->doing_php_initial_state();

Without immediately looking at the code under the hood (we’ll do that later), consider two features of the Context:

  1. It’s available anywhere in the code ‐ just call the tribe_context() function.
  2. It’s immutable.

That first point means that the tribe_context() function will consistently return an instance of the Tribe__Context object as long as The Events Calendar plugin is loaded (well, technically, the common libraries it shares with Event Tickets).

The second point means that, yes, that same instance is shared among all the code calling the tribe_context() function, but the only way to alter the Context is to call the Tribe__Context::alter() method.

This second point is better shown with an example:

<?php
$global_context = tribe_context();

// This is a clone of the global context, no modification was done to the global context!
$jan_2020_context = tribe_context()->alter( [ 'event_date' => '2020-01' ] );

// This is a clone of the global context, no modification was done to the global context!
$feb_2020_context = tribe_context()->alter( [ 'event_date' => '2020-02' ] );

assert( $global_context !== $jan_2020_context );
assert( $global_context !== $feb_2020_context );
assert( $jan_2020_context !== $feb_2020_context );

The advantage this offers is that given code A and B that only use the Context as their source of truth, no change to the context of A can be done by code executing in code B.

Context immutability, and its implications, would require a separate dedicated article, but the takeaway here is that, while the Context is globally available, it’s not globally modifiable.

ORM, or Repository

If you see the word “repository” in the example plugin code know we’re talking about the “ORM” implementation.

What does “ORM” mean? look no further than Wikipedia for a definition:

[…] a programming technique for converting data between incompatible type systems using object-oriented programming languages. This creates, in effect, a “virtual object database” that can be used from within the programming language.

Nice. What does it mean from a developer perspective?

Let’s look at an example where we want to fetch all events that:

  • start after 2020-01-01
  • end before 2020-01-31
  • are published
  • allow walk-in purchases (modeled using the _walkin_purchases custom field)
  • take place in a specific Venue.
  • are organized by a specific Organizer
  • use the “Party” category
  • are tagged with “Casual Friday.”

First, let’s use a default WordPress WP_Query:

<?php
$events = get_posts( [
  'post_type'   => 'tribe_events',
  'post_status' => 'publish',
  'meta_query'  => [
    'relation'      => 'AND',
    'starts_after'  => [
      'key'     => '_EventStartDate',
      'compare' => '>=',
      'value'   => '2020-01-01 00:00:00',
    ],
    'ends_before'   => [
      'key'     => '_EventEndDate',
      'compare' => '<=',
      'value'   => '2020-01-31 23:59:59',
     ],
     'timezone'      => [
       'key'     => '_EventTimezone',
       'compare' => '=',
       'value'   => 'Europe/Paris',
      ],
      'allows_walkin' => [
        'key'     => '_walkin_purchases',
        'compare' => 'EXISTS',
      ],
      'organizer' => [
        'key'     => '_EventOrganizerID',
        'compare' => '=',
        'value'   => $organizer_id,
      ],
      'venue' => [
        'key'     => '_EventVenueID',
        'compare' => '=',
        'value'   => $venue_id,
      ],
    ],
    'tax_query'   => [
      'relation' => 'AND',
      'category' => [
        'taxonomy' => 'tribe_events_cat',
        'field'    => 'slug',
        'terms'    => 'party',
      ],
      'tag'      => [
        'taxonomy' => 'post_tag',
        'field'    => 'name',
        'terms'    => 'Casual Friday',
      ],
  ],
] );

Now let’s do the same using the Events ORM:

$events = tribe_events()
  ->where( 'status', 'publish' )
  ->where( 'starts_after', '2020-01-01 00:00:00' )
  ->where( 'ends_before', '2020-01-31 23:59:59' )
  ->where( 'timezone', 'Europe/Paris' )
  ->where( 'meta_exists', '_walkin_purchases' )
  ->where( 'event_category', 'party' )
  ->where( 'tag', 'Casual Friday' )
  ->where( 'organizer', $organizer_id )
  ->where( 'venue', $venue_id )
  ->all();

Beside more concise and readable code, the second example abstracts the code away from “how” the data is stored to concentrate on “what” we’re trying to achieve.

The “status” of a post is a field of the posts table while the other values are all entries related to the post, by its ID, in the postmeta and terms tables.

This implementation details, where is the piece of information we’re looking for stored and how “leaks” in the WP_Query example requiring knowledge of “how” the data is stored, to fetch it.

In the ORM example, we’re instead stating “what” we want and let the ORM implementation deal with building the query with knowledge of “how” the information is stored.

You’ve probably noticed we passed the Venue and Organizer we want to filter by using their post IDs. How did we get those?

Well, we can use the ORM for Venues and Organizers too:

$venue_id     = tribe_venues()
    ->where( 'title_like', 'The Cove' )
    ->first();
$organizer_id = tribe_organizers()
    ->where( 'title_like', 'Luca' )
    ->first();

The first() method means “return only the first result”. More methods like this exist, including:

  • offset( $by_how_many ) to offset the results by value.
  • last() to get the last result.
  • take( $how_many ) to get, at the most, n results

And many more. That can be combined too.

This is just a quick look at the ORM and what it can do, but the takeaway is the ORM allows you to decide what you want, not how to get it by abstracting low-level query building away.

The code, step by step

Now that all the introduction is done, it’s time to “walk” through the code and understand what we’re doing.

As a first step we’re creating the main, and only, plugin class, instantiating it and hooking it to the required filters and actions.

<?php
/**
 * Plugin Name: Walk-in purchase View filter.
 * Plugin Description: Show/hide events based on walk-in purchase availability in The Events Calendar V2 Views.
 */

namespace Acme\Walkin_Event_Purchases;

class Plugin {
  // [...]
}

// Instantiate the plugin.
$plugin = new Plugin;

// Hook the plugin to the filters and actions required by it to work.
$plugin->hook();

In the Plugin::hook() method, we’re adding filters and actions (which we’ll look at in more detail in a bit) as we move through the code. Let’s take a quick glance now and then move on:

<?php
namespace Acme\Walkin_Event_Purchases;

class Plugin {
  // [...]

  /**
   * Hooks the filters and actions required for the plugin to work.
   */
  public function hook() {
    add_filter( 'tribe_context_locations', [ $this, 'filter_context_locations' ] );
    add_action(
      'tribe_template_before_include:events/v2/components/events-bar',
      [ $this, 'print_frontend_controls' ]
    );
    add_filter( 'tribe_events_views_v2_url_query_args', [ $this, 'filter_view_url_query_args' ] );
    add_filter( 'tribe_events_views_v2_view_repository_args', [ $this, 'filter_view_repository_args' ] );

    // Admin section, this does not take care of the block-editor. That's left as an exercise to the reader.
    add_action( 'tribe_events_cost_table', [ $this, 'print_backend_controls' ] );
    add_action( 'save_post_tribe_events', [ $this, 'save_meta' ] );
  }
}

Adding the custom meta field

In this example plugin, we want to allow the site administrator, or editor, to set an event as one that allows walk-in ticket purchases.

Not all events offer this possibility, so we want to allow site visitors to filter a View (Month, List etc.) events to either show all events or only show the events that allow walk-in ticket purchases.

We add two actions: the Plugin::hook() method:

  • To show a checkbox on the event editor screen for the site administrator to set an event as one allowing walk-in purchases or not.
  • To handle that checkbox value when the post is saved and either store its value in the _walkin_purchases custom field, or not store the custom field at all (and delete it if previously set).
<?php
namespace Acme\Walkin_Event_Purchases;

class Plugin {

  // [...]
    
  /**
   * Prints the HTML fragment required to display a checkbox in the Events edit screen, in the "Cost" section.
   *
   * @param int $event_id The post ID of the event currently being edited.
   */
  public function print_backend_controls( $event_id ) {
    $html_template = '<tr>' .
                     '<td>%s</td>' .
                     '<td><input type="checkbox" name="%s" %s></td>' .
                     '</tr>';

    printf(
      $html_template,
      esc_html( 'Allow walk-in purchases:' ),
      self::META_KEY,
      checked( true, (bool) get_post_meta( $event_id, self::META_KEY, true ) )
    );
  }

  /**
   * Handles the value of the checkbox added in the `print_walk_in_checkbox` method to either set the meta or
   * delete it if the checkbox is not checked.
   *
     * @param int $event_id The post ID of the event currently being saved.
     */
    public function save_meta( $event_id ) {
        $allow_walkin_purchases = tribe_get_request_var( self::META_KEY, false );
      if ( false === $allow_walkin_purchases ) {
        delete_post_meta( $event_id, self::META_KEY );
      } else {
        update_post_meta( $event_id, self::META_KEY, true );
      }
    }

  /**
   * Hooks the filters and actions required for the plugin to work.
   */
  public function hook() {
    // [...]

    // Admin section, this does not take care of the block-editor. That's left as an exercise to the reader.
    add_action( 'tribe_events_cost_table', [ $this, 'print_backend_controls' ] );
    add_action( 'save_post_tribe_events', [ $this, 'save_meta' ] );
  }
}

The end result is this simple UI:

Reading the filter query variable

Before looking at the front-end controls, a simple link, we need to “wire” our plugin to make sure the View events are filtered depending on the presence of the wip-only query variable or not.

If the variable is not present then all events should show, if the variable is present, then only events that allow walk-in purchases should show in the View.

The go-to idea is to simply read the wip-only variable from the GET request, but that would only partially cover it.

During PHP initial state, the first HTTP GET request sent when the user first visits the /events page, looking for the $_REQUEST['wip-only'] variable would work, but successive user interactions with the Views will not trigger a page refresh: the Views content will be requested to the site with AJAX via either the REST API, or, if the REST API is disabled, via a request to /wp-admin/admin-ajax.php.

This adds more places where we have, now, to look for the wip-only query variable; furthermore, we’ll need to look in specific places depending on the request being a PHP initial state one, a REST API powered AJAX request or a wp-admin/admin-ajax.php powered AJAX request.

What if we “hide” all this complexity in the Context and add a walkin_purchases_only location?

Doing this all the “looking around” mentioned above would live in a single place and its complexity hidden.

The Context implementation easily allows and supports that! In the Plugin::hook() we filter the Context locations:

<?php

namespace Acme\Walkin_Event_Purchases;

class Plugin {

  // [...]

  public function hook() {
    add_filter( 'tribe_context_locations', [ $this, 'filter_context_locations' ] );
  }
}

In the Plugin::filter_context_locations() method, we hide all the information gathering complexity mentioned above.

Read the inline comments for more punctual information:

<?php
public function filter_context_locations( array $context_locations = [] ) {
  $context_locations[ self::CONTEXT_LOCATION ] = [

    // Add a reading location to the Context, we'll not need a write one.
    // Locations are looked from the first to the last, until one finds a value.
    'read' => [

      // First of all look into  `$_REQUEST`.
      \Tribe__Context::REQUEST_VAR   => self::REQUEST_VAR,

      // Then look into the View data, the one sent along by the View during AJAX requests.
      \Tribe__Context::LOCATION_FUNC => [
      'view_data',
      static function ( $data ) {
        return is_array( $data ) && ! empty( $data['wip-only'] ) ?
        true : \Tribe__Context::NOT_FOUND;
      },
    ],

    // Finally look in the URL that some View requests will send along.
    \Tribe__Context::FUNC          => static function () {
      $url = tribe_get_request_var( 'url', false );
        if ( empty( $url ) ) {
          return \Tribe__Context::NOT_FOUND;
        }
        $query_string = wp_parse_url( $url, PHP_URL_QUERY );
        wp_parse_str( $query_string, $query_args );

        return ! empty( $query_args['wip-only'] ) ? true : \Tribe__Context::NOT_FOUND;
      }
    ]
  ];

  return $context_locations;
}

There are some important takeaways in this code:

  1. The Context allows defining multiple, cascading ways to read a location. In this example we look into $_REQUEST, then into the View Data, then into the $url parameter the View sends along in requests.
  2. The Context can consume its own information. In the second entry we say, using the Tribe__Context::LOCATION _FUNC constant, we want the Context to first fetch information from the view_data location. This is a buit-in location that is, on its own, already abstracting an amount of information.
  3. The Context supports, out of the box, a number of look-up methods. The REQUEST_VAR method calls, under the hood, tribe_request_var (a safe function to read from request globals); the LOCATION_FUNC method passes another location value to a callback, the FUNC method just calls the specified callback.

There are a ton more lookup methods supported by Tribe__Context we’re not listing for brevity, take some time to look at the class code and documentation to find out more.

Having done this we can now call this code to get the value of the wip-only query variable in the context (lower case “c”) of the current request:

// Default to `false` if not found.
$wip_only = tribe_context()->get( 'walkin_purchases_only', false );

That is what we do in the Plugin::filter_view_repository_args method:

public function filter_view_repository_args( array $repository_args = [] ) {
  // Only add the repository argument if the flag is set.
  if ( tribe_context()->get( 'walkin_purchases_only', false ) ) {
    $repository_args['meta_exists'] = '_walkin_purchases';
  }

  return $repository_args;
}

The method is hooked to the tribe_events_views_v2_view_repository_args filter:

<?php

namespace Acme\Walkin_Event_Purchases;

class Plugin {

  /**
   * Hooks the filters and actions required for the plugin to work.
   */
  public function hook() {
    // [...]

    add_filter( 'tribe_events_views_v2_view_repository_args', [ $this, 'filter_view_repository_args' ] );
  }
}

The tribe_events_views_v2_view_repository_args filter allows us to add a Repository argument to the criteria by which the View will fetch the events from the database.

The meta_exists filter is an ORM built-in one: it will only keep events that, beside matching each View own filtering criteria, also have the _walkin_purchases custom fieldset.

Now that this is wired, it is time to add some minimal UI to the View front-end to allow users to filter events in a way more comfortable than adding ?wip-only=1 to the View URL.

In the Plugin::filter_view_url_query_args method, we just make sure the View URL will keep the wip-only query var no matter the nature of the HTTP request (PHP initial state, AJAX over REST, AJAX over wp-admin/admin-ajax.php).

Displaying the link

You can look up the complete code at the end of this article, but it’s worth pointing out some points of the code responsible for rendering the filtering link:

<?php

namespace Acme\Walkin_Event_Purchases;

class Plugin {

    /**
     * Prints the HTML for the plugin front-end controls.
     *
     * Currently just a link with dynamic text.
     */
    public function print_frontend_controls() {
        // Let's get a hold of the current global context.
        $context = tribe_context();

        // Here we use, again, the custom location we added.
        $wip_only = $context->get( self::CONTEXT_LOCATION, false );
        // And some more built-in locations.
        $date     = $context->get( 'event_date', false );
        $view_url = $context->get( 'view_url', false );

        // The ORM supports "natural language" for the dates!
        $query_date = $date ?: 'now';

        // We use `found` to get the total count, ignoring pagination.
        $with_walkin     = tribe_events()
            ->where( 'meta_exists', self::META_KEY )
            ->where( 'ends_after', $query_date )
            ->found();
        $upcoming_events = tribe_events()
            ->where( 'ends_after', $query_date )
            ->found();

        $base_url = empty( $view_url ) ? add_query_arg( [] ) : $view_url;

        if ( $wip_only ) {
            $url   = remove_query_arg( self::REQUEST_VAR, $base_url );
            $label = sprintf(
                'Show all events (showing %d with walk-in only of %d upcoming events)',
                $with_walkin,
                $upcoming_events
            );
        } else {
            $url   = add_query_arg( [ self::REQUEST_VAR => '1' ], $base_url );
            $label = sprintf(
                'Only show events with walk-in ticket purchase (%d of %d upcoming events)',
                $with_walkin,
                $upcoming_events
            );
        }

        printf(
            ' <a href="%s" data-js="tribe-events-view-link" style="%s">%s</a>',
            esc_url( $url ),
            'text-decoration: underline; color: #334AFF; margin-bottom: .25em; font-size: 80%;',
            esc_html( $label )
        );
    }

    /**
     * Hooks the filters and actions required for the plugin to work.
     */
    public function hook() {
        add_action(
            'tribe_template_before_include:events/v2/components/events-bar',
            [ $this, 'print_frontend_controls' ]
        );

        // [...]
    }
}

To show the upcoming events with and w/o walk-in ticket purchase, we use the ORM to count the events. The found() method will run a fast and optimized query to count matching events. To fetch the events, with respect to the date, we read the date requested by the user.

The event_date location is built-in in the Context and abstracts away the look-up of a number of different locations (query vars, default values, WP parsed values and so on) to provide us with a date.

If no date was set, i.e. the user just visited the View main page without picking a date, we use “now.”

This highlights a specific feature of the Events ORM when it comes to dates: it supports natural language.

All the below queries are valid:

tribe_events()->where( 'starts_after', 'now' );
tribe_events()->where( 'ends_after', 'tomorrow 9am' );
tribe_events()->where( 'starts_between', '-2 weeks', '+2 weeks' );

Finally, we tap into a feature of the View architecture to have the front-end JavaScript code intercept and handle clicks on the link.

By default, a click on the link would trigger a full page refresh, but adding the data-js="tribe-events-view-link" attribute to the anchor tag will include the link among the links the View considers its own and manages.

The plugin code

Below is the full code of the plugin, ready for copy and paste.

Create a tec-walk-in-purchase directory in your WordPress plugins directory (/wp-content/plugins) and copy the code below to a plugin.php file inside it.

<?php
/**
* Plugin Name: Walk-in purchase View filter.
* Plugin Description: Show/hide events based on walk-in purchase availability in The Events Calendar V2 Views.
*/

namespace Acme\Walkin_Event_Purchases;

class Plugin {

  /**
   * The meta key used to store the site editor choice in regard to an event support for walk-in purchases.
   */
  const META_KEY = '_walkin_purchases';

  /**
   * The name of the Context location we'll read to know if the visitor wishes to see only events that allow walk-in
   * purchases only or not.
   */
  const CONTEXT_LOCATION = 'walkin_purchases_only';

  /**
   * The name of the query variable appended to a View URL to indicate whether to show all events or only those that
   * allow walk-in purchase. Short and slug friendly to avoid too long URLs.
   */
  const REQUEST_VAR = 'wip-only';

  /**
   * Plugin constructor.
   * The method will register the plugin instance in the `walkin_purchases` to allow global access to the instance.
   */
  public function __construct() {
     global $walkin_purchases;
     $walkin_purchases = $this;
  }

  /**
   * Adds the `walkin_purchases_only` location to the Context.
   *
   * This will allow us to call `tribe_context()->get( 'walkin_purchases_only' )` to know whether the user would
   * like to see all events or only those that allow walk-in purchases.
   *
   * @param array<string,array> $context_locations A list of Context "locations".
   *
   * @return array<string,array> The filtered Context locations.
   */
  public function filter_context_locations( array $context_locations = [] ) {
     $context_locations[ self::CONTEXT_LOCATION ] = [
        'read' => [
           \Tribe__Context::REQUEST_VAR   => self::REQUEST_VAR,
           \Tribe__Context::LOCATION_FUNC => [
              'view_data',
              static function ( $data ) {
                 return is_array( $data ) && ! empty( $data['wip-only'] ) ?
                    true : \Tribe__Context::NOT_FOUND;
              },
           ],
           \Tribe__Context::FUNC          => static function () {
              $url = tribe_get_request_var( 'url', false );
              if ( empty( $url ) ) {
                 return \Tribe__Context::NOT_FOUND;
              }
              $query_string = wp_parse_url( $url, PHP_URL_QUERY );
              wp_parse_str( $query_string, $query_args );

              return ! empty( $query_args['wip-only'] ) ? true : \Tribe__Context::NOT_FOUND;
           }
        ]
     ];

     return $context_locations;
  }

  /**
   * Prints the HTML for the plugin front-end controls.
   *
   * Currently just a link with dynamic text.
   */
  public function print_frontend_controls() {
     // Let's get a hold of the current global context.
     $context = tribe_context();

     //
     $wip_only = $context->get( self::CONTEXT_LOCATION, false );
     $date     = $context->get( 'event_date', false );
     $view_url = $context->get( 'view_url', false );

     $query_date = $date ?: 'now';

     // We use `found` to get the total count, ignoring pagination.
     $with_walkin     = tribe_events()
        ->where( 'meta_exists', self::META_KEY )
        ->where( 'ends_after', $query_date )
        ->found();
     $upcoming_events = tribe_events()
        ->where( 'ends_after', $query_date )
        ->found();

     $base_url = empty( $view_url ) ? add_query_arg( [] ) : $view_url;

     if ( $wip_only ) {
        $url   = remove_query_arg( self::REQUEST_VAR, $base_url );
        $label = sprintf(
           'Show all events (showing %d with walk-in only of %d upcoming events)',
           $with_walkin,
           $upcoming_events
        );
     } else {
        $url   = add_query_arg( [ self::REQUEST_VAR => '1' ], $base_url );
        $label = sprintf(
           'Only show events with walk-in ticket purchase (%d of %d upcoming events)',
           $with_walkin,
           $upcoming_events
        );
     }

     printf(
        ' <a href="%s" data-js="tribe-events-view-link" style="%s">%s</a>',
        esc_url( $url ),
        'text-decoration: underline; color: #334AFF; margin-bottom: .25em; font-size: 80%;',
        esc_html( $label )
     );
  }

  /**
   * Filters the View query arguments, those the View will add to its URL, to make sure the user choice to show
   * only events that allow walk-in ticket purchases or not is reflected in the URL.
   *
   * @param array<string,string> $query_args The list of query arguments the View will append to its URL.
   *
   * @return array<string,string> The filtered list of query arguments the View will append to its URL; we add
   *                              the `wip-only` one if the user wants to show only events that allow walk-in ticket
   *                              purchase.
   */
  public function filter_view_url_query_args( array $query_args = [] ) {
     if ( tribe_context()->get( self::CONTEXT_LOCATION, false ) ) {
        $query_args[ self::REQUEST_VAR ] = '1';
     } else {
        unset( $query_args[ self::REQUEST_VAR ] );
     }

     return $query_args;
  }

  /**
   * Filters the View repository arguments, the one the View will use to fetch events, to take into account the user
   * choice to show only events that allow walk-in purchases or not.
   *
   * @param array<string,mixed> $repository_args The original list of repository arguments the View will use to
   *                                             fetch events.
   *
   * @return array<string,mixed> The filtered list of View repository arguments. We'll add one related to the
   *                             `_walkin_purchases` existence only if the user wants to only show events that allow
   *                             walk-in ticket purchase or not.
   */
  public function filter_view_repository_args( array $repository_args = [] ) {
     if ( tribe_context()->get( self::CONTEXT_LOCATION, false ) ) {
        $repository_args['meta_exists'] = self::META_KEY;
     }

     return $repository_args;
  }

  /**
   * Prints the HTML fragment required to display a checkbox in the Events edit screen, in the "Cost" section.
   *
   * @param int $event_id The post ID of the event currently being edited.
   */
  public function print_backend_controls( $event_id ) {
     $html_template = '<tr>' .
                      '<td>%s</td>' .
                      '<td><input type="checkbox" name="%s" %s></td>' .
                      '</tr>';

     printf(
        $html_template,
        esc_html( 'Allow walk-in purchases:' ),
        self::META_KEY,
        checked( true, (bool) get_post_meta( $event_id, self::META_KEY, true ) )
     );
  }

  /**
   * Handles the value of the checkbox added in the `print_walk_in_checkbox` method to either set the meta or
   * delete it if the checkbox is not checked.
   *
   * @param int $event_id The post ID of the event currently being saved.
   */
  public function save_meta( $event_id ) {
     $allow_walkin_purchases = tribe_get_request_var( self::META_KEY, false );
     if ( false === $allow_walkin_purchases ) {
        delete_post_meta( $event_id, self::META_KEY );
     } else {
        update_post_meta( $event_id, self::META_KEY, true );
     }
  }

  /**
   * Hooks the filters and actions required for the plugin to work.
   */
  public function hook() {
     add_filter( 'tribe_context_locations', [ $this, 'filter_context_locations' ] );
     add_action(
        'tribe_template_before_include:events/v2/components/events-bar',
        [ $this, 'print_frontend_controls' ]
     );
     add_filter( 'tribe_events_views_v2_url_query_args', [ $this, 'filter_view_url_query_args' ] );
     add_filter( 'tribe_events_views_v2_view_repository_args', [ $this, 'filter_view_repository_args' ] );

     // Admin section, this does not take care of the block-editor. That's left as an exercise to the reader.
     add_action( 'tribe_events_cost_table', [ $this, 'print_backend_controls' ] );
     add_action( 'save_post_tribe_events', [ $this, 'save_meta' ] );
  }
}

// Instantiate the plugin.
$plugin = new Plugin;

// Hook the plugin to the filters and actions required by it to work.
$plugin->hook();

    Details

    Report an issue