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 ) |
Replace the one implementation resolved by evnm( Contract::class ). E.g. swap a repository. Later bindings win. |
tag( Contract::class, MyImpl::class ) |
Add to a collection that is consumed with evnm()->tagged( Contract::class ). |
Only two contracts are actually consumed as tagged collections:
NotificationChannelInterface(the notifier fans out to every tagged channel) andRecurrenceEngineInterface(the materializer dispatches to the first tagged engine whosehandles_rule()returns true). Everything else in the container is a singlebind().Payment gateways are the exception to both. A gateway is not wired through the container; tagging
PaymentGatewayInterfacedoes not make a gateway appear. The checkout flow reads two filters instead:evnm_payment_gateways(advertise the gateway) andevnm_process_payment(run the charge). See the gateway example below.
Example: Adding a Payment Gateway (Filters, Not tag())
Implementing PaymentGatewayInterface gives you a clean object model, but registration is done with filters, exactly as Pro's Stripe/PayPal/Woo gateways do it.
<?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' ]; }
// charge() returns an array with 'transaction_id' and 'status' (or a WP_Error).
public function charge( array $order, array $args ) {
return [ 'transaction_id' => 'door-' . $order['order_number'], 'status' => 'paid' ];
}
public function capture( string $transaction_id, array $args ) {
return [ 'transaction_id' => $transaction_id, 'status' => 'paid' ];
}
// Real signature: refund( string, int, string ): array|WP_Error.
public function refund( string $transaction_id, int $amount_cents, string $currency = '' ) {
return [ 'transaction_id' => $transaction_id, 'status' => 'refunded' ];
}
}
<?php
// my-addon/my-addon.php (or a provider file)
// 1) Advertise the gateway so checkout knows a paid order can be taken.
add_filter( 'evnm_payment_gateways', function ( $gateways ) {
$gateways = (array) $gateways;
$gateways['my-gateway'] = [ 'id' => 'my-gateway', 'label' => __( 'Pay at Door', 'my-addon' ) ];
return $gateways;
} );
// 2) Actually run the charge when this gateway is selected. Return an array with
// 'transaction_id' (+ 'status'); return the prior $result if it's not yours.
add_filter( 'evnm_process_payment', function ( $result, $order, $input ) {
if ( null !== $result ) {
return $result; // Another gateway already handled it.
}
if ( 'my-gateway' !== ( $input['gateway'] ?? '' ) ) {
return $result;
}
return ( new \MyAddon\MyGateway() )->charge( $order, $input );
}, 10, 3 );
Once both filters are wired, the gateway appears in the checkout selector and settles paid orders.
Example: Adding a Recurrence Engine (tag(), Not bind())
The recurrence engine is a tagged collection: Free tag()s its basic engine, and the materializer reads evnm()->tagged( RecurrenceEngineInterface::class ) and dispatches to the first engine whose handles_rule() returns true. So you add an engine with tag(); a bind() here would be ignored (nothing calls evnm( RecurrenceEngineInterface::class )).
add_action( 'evnm_register_services', function ( $container ): void {
// tag(), not bind(): the materializer consumes the tagged collection.
$container->tag(
\Eventonomy\Contracts\RecurrenceEngineInterface::class,
\MyAddon\MyRecurrenceEngine::class
);
} );
MyRecurrenceEngine must implement the full interface:
handles(): array- the freq tokens it supports (e.g.[ 'DAILY', 'WEEKLY' ]).handles_rule( array $rule ): bool- whether it can correctly expand this exact rule (returnfalsefor advanced parts it can't honour, so a more capable engine wins the dispatch).expand( array $rule, \DateTimeImmutable $start, \DateTimeImmutable $window_end ): array- the concrete occurrences.
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, for a tagged collection (notification channel or recurrence engine):
// my-addon/includes/Providers/MyChannelProvider.php
add_action( 'evnm_register_services', function ( $container ): void {
$container->tag(
\Eventonomy\Contracts\NotificationChannelInterface::class,
\MyAddon\Services\SlackChannel::class
);
} );
This pattern means adding a new service never requires editing a shared registrar, exactly how Pro adds features to Free. (A payment gateway provider registers the two filters from the gateway example above, not a tag().)
Resolving Services from the Container
// Single bound contract - evnm() returns the one implementation.
$repo = evnm( \Eventonomy\Contracts\EventRepositoryInterface::class );
// Tagged collection - use ->tagged(). Calling evnm() on a tag-only id THROWS
// (nothing is bound for it), so always resolve collections with tagged():
$channels = evnm()->tagged( \Eventonomy\Contracts\NotificationChannelInterface::class );
// -> array of every tagged NotificationChannelInterface implementation.
Never use new on a concrete class you don't own. Resolve contracts through evnm(): bind() a single contract, tagged() a collection. (Note: evnm() can resolve some concrete Services\* classes that are explicitly bound, e.g. evnm( \Eventonomy\Services\EventService::class ) for the documented EventService::create() entry point, but prefer interfaces everywhere else so a later swap doesn't break you.)
Verify It Worked
- Activate your add-on plugin.
- For a gateway: go to Settings → Money → Payments and confirm your gateway appears in the Active gateway dropdown (this proves both
evnm_payment_gatewaysandevnm_process_paymentare wired). - For a tagged collection: write a small
wp evalcommand that lists the resolved classes:wp eval 'var_dump( array_map( "get_class", evnm()->tagged( \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