Recipe: Extend with a Provider
Recipe: Extend with a Provider
Section titled “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
Section titled “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()
Section titled “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())
Section titled “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.
<?phpnamespace 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())
Section titled “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)
Section titled “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:
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):
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
Section titled “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
Section titled “Verify It Worked”- Activate your add-on plugin.
- For a gateway: go to Settings → Payments → 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:Terminal window wp eval 'var_dump( array_map( "get_class", evnm()->tagged( \Eventonomy\Contracts\RecurrenceEngineInterface::class ) ) );'
Related
Section titled “Related”docs/EXTENDING.md§3 - full Contract reference and container API.docs/ARCHITECTURE.md- the auto-discovery backbone and provider loading.- Recipes Index

