Eventonomy

Recipe: Extend with a Provider

Goal: Add a service to Eventonomy's container using the auto-discovery provider pattern - the same pattern every Pro feature uses. The provider wires itself into the container with no changes to shared registrar files.

Seams Used

Seam Type Purpose
evnm_register_services Action Fires after the container loads all providers; your callback receives $container.
$container->bind() Method Replace the container's one binding for a contract. Later bindings win (Pro layers on Free this way).
$container->tag() Method Add an implementation to a collection (e.g. add a gateway alongside existing ones).
evnm() Function Public container accessor - resolve any bound contract anywhere.

When to Use bind() vs tag()

Method Use when
bind( Contract::class, MyImpl::class ) You want to replace the single bound implementation. E.g. swap the recurrence engine.
tag( Contract::class, MyImpl::class ) You want to add to a collection. E.g. add a payment gateway alongside Stripe.

Example - Adding a Payment Gateway

<?php
// my-addon/includes/MyGateway.php
namespace MyAddon;

class MyGateway implements \Eventonomy\Contracts\PaymentGatewayInterface {
    public function id(): string { return 'my-gateway'; }
    public function label(): string { return 'Pay at Door'; }
    public function supported_currencies(): array { return [ 'USD', 'EUR' ]; }

    public function charge( array $order, array $args ): array {
        // Record the promise; no charge collected now.
        return [ 'txn_id' => 'door-' . $order['id'], 'status' => 'completed' ];
    }

    public function capture( string $txn, array $args ): array {
        return [ 'txn_id' => $txn, 'status' => 'completed' ];
    }

    public function refund( string $txn, int $cents ): bool {
        // No payment was collected, so nothing to refund.
        return true;
    }
}
<?php
// my-addon/my-addon.php (or a provider file)

add_action( 'evnm_register_services', function ( $container ): void {
    // tag() adds to the gateway collection; does not replace Stripe.
    $container->tag(
        \Eventonomy\Contracts\PaymentGatewayInterface::class,
        \MyAddon\MyGateway::class
    );
} );

The gateway appears automatically in the checkout gateway selector once registered.

Example - Swapping the Recurrence Engine

add_action( 'evnm_register_services', function ( $container ): void {
    // bind() replaces the single bound implementation.
    $container->bind(
        \Eventonomy\Contracts\RecurrenceEngineInterface::class,
        \MyAddon\MyRecurrenceEngine::class
    );
} );

MyRecurrenceEngine must implement handles(): array and expand($rule, $start, $window_end): array.

Using the Auto-Discovery Provider Pattern (Recommended)

The cleanest approach for plugins distributing multiple services: create a Providers/ directory and a file per provider. Eventonomy's own bootstrap scans includes/Providers/*.php via glob. You can mirror this in your plugin:

// my-addon/my-addon.php
foreach ( glob( plugin_dir_path( __FILE__ ) . 'includes/Providers/*.php' ) as $provider ) {
    require_once $provider;
}

Each provider file registers its own hook:

// my-addon/includes/Providers/MyGatewayProvider.php
add_action( 'evnm_register_services', function ( $container ): void {
    $container->tag(
        \Eventonomy\Contracts\PaymentGatewayInterface::class,
        \MyAddon\Services\MyGateway::class
    );
} );

This pattern means adding a new service never requires editing a shared registrar - exactly how Pro adds features to Free.

Resolving Services from the Container

// Later in any hook - resolves the concrete from the container by contract.
$gateway_registry = evnm( \Eventonomy\Contracts\PaymentGatewayInterface::class );
// Returns an array of all tagged implementations.

$repo = evnm( \Eventonomy\Contracts\EventRepositoryInterface::class );
// Returns the single bound implementation.

Never use new on a concrete class. Always resolve through evnm().

Verify It Worked

  1. Activate your add-on plugin.
  2. For a gateway: go to Settings → Money → Payments and confirm your gateway appears in the Active gateway dropdown.
  3. For a service swap: write a small wp eval command that resolves the contract and checks get_class():
    wp eval 'var_dump(get_class(evnm(\Eventonomy\Contracts\RecurrenceEngineInterface::class)));'
    

Related

  • docs/EXTENDING.md §3 - full Contract reference and container API.
  • docs/ARCHITECTURE.md - the auto-discovery backbone and provider loading.
  • Recipes Index