PHP code example of calvinalkan / better-wordpress-hooks

1. Go to this page and download the library: Download calvinalkan/better-wordpress-hooks library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

calvinalkan / better-wordpress-hooks example snippets


// Approach #1, creating the class and passing the object to the hook.
class MyClass {

    public function __construct() {
        add_action('init', [ $this, 'doStuff']);
    }
    public function doStuff () {
        //
    }
    
    public static function doStuffStatic () {
    //
    }
}

// Approach #2, Using static functions, worse.
add_action('init', [ MyClass::class, 'doStuffStatic' ]);

//Let's assume we want to send an email and log to an external service
// ( maybe a google sheet ) every time an order takes place with a total > 500. 
class GiftCardHandler {

    private $mailer; 
    private $logger;

    // We need a mailer and a logger service
    public function __construct( Mailer $mailer, Logger $logger) {
    
        $this->mailer = $mailer; 
        $this->logger = $logger; 
        add_action('checkout_completed', [ $this, 'handle'], 10, 1);
        
    }
    
    //Let's assume we get an order object from the hook.
    public function handle (Order $order) {
    
        if ( $order->total() >= 500 ) {
        
            $mailer->sendGiftcard($order->user());
            $logger->logBigBurchase($order);
        
        }
    }
}

use BetterWpHooks\Traits\BetterWpHooksFacade;

class AcmeEvents {

use BetterWpHooksFacade;

}

use BetterWpHooks\Traits\BetterWpHooksFacade;

class AcmeEvents {

use BetterWpHooksFacade;

}

AcmeEvents::make();

// Alternatively if you want to use a custom container:
$custom_container_adapter = new SymfonyContainerAdapter();
AcmeEvents::make($custom_container_adapter);

$mapped = [

// Will fire on priority 10
'init' =>  RegisterJobListingPostType::class,

// Will fire on priority 99
'save_post_job_listing' => [JobListingCreated::class, 99],

// Will be resolved from the service container
// and fire on priority 99.
'booking_created' => ['resolve' ,BookingCreated::class, 99],

];

AcmeEvents::mapEvents($mapped);

$listeners = [

RegisterJobListingPostType::class => [

    PostTypeRegistry::class,
    
    // More Event Listeners if needed
], 

JobListingCreated::class => [

   NotifyAdmin::class,
   SendConfirmationEmail::class, 
   TagCreatorInMailchimp::class
   
    // More Event Listeners if needed
],

// Custom Events that you fire in your code 
JobMatchFound::class => [

    NotifyListingOwner::class,
    NotifyApplicatant::class,

]

];

AcmeEvents::listeners($listeners);
AcmeEvents::boot();



$mapped = = make()->mapEvents($mapped)->listeners($listeners)->boot();

class HighValueOrderCreated extends AcmeEvents {

    public $order;
    public $submission_data;
    
   public function __construct( int $order_id, array $submission_data ) {
   
            $this->order = wc_get_order($order_id);
            $this->submission_data = $submission_data;
   }

}

class ProcessGiftCards {

    private $mailer;
    private $logger;
    
   public function __construct( $mailer, $logger ) {
   
            $this->mailer = $mailer;
            $this->logger = $logger;
   }
   
   public function handleEvent ( HighValueOrderCreated $event) {
   
        $order = $event->order; 
        
        // Example properties. 
        $this->mailer->sendGiftCard( $order->user_id, $order->items );
        $this->logger->logGiftcardRecepient( $order->user_id, $order->items, $order->purchased_at );
        
   }

}



$mapped = [

    'woocommerce_checkout_order_processed' => [
    
        HighValueOrderCreated::class 
        // Map more if needed.
]];

$listeners = [
    HighValueOrderCreated::class => [
    
        ProcessGiftCards::class . '@handleEvent'
        // More Listeners if needed
] ];

AcmeEvents::make()->mapEvents($mapped)->listeners($listeners)->boot();

class HighValueOrderCreated extends AcmeEvents {

    use \BetterWpHooks\Traits\DispatchesConditionally;

    public $order;
    public $submission_data;
    
    public function __construct( int $order_id, array $submission_data ) {
   
            $this->order = wc_get_order($order_id);
            $this->submission_data = $submission_data;
   }

    public function shouldDispatch() : bool{
 
    return $this->order->total >= (int) get_option('acme_gift_card_amount');
 
}}


use Illuminate\Container\Container;
use SniccoAdapter\BaseContainerAdapter;
use MailerInterface;

$container = new Container();

$container->when(ProcessGiftCards::class)
           ->needs(MailerInterface::class)
           ->give(function () {
           
              $mailer = get_option('acme_mailer');
              
              if($mailer === 'sendgrid') {
                
                return new SendGridMailer();
              
              }  
              
              if($mailer === 'mailgun') {
                
                return new MailGunMailer();
              
              }  
                
              return new AwsMailer();
              
          });

AcmeEvents::make(new BaseContainerAdapter($container)); // -> 

// Code that processes an appointment booking. 

do_action('booking_created', $book_id, $booking_data );

// Code that processes an appointment booking. 

BookingCreated::dispatch( [$book_id, $booking_data] );

class AcmeEvents {

use BetterWpHooksFacade;

}
 
BookingCreated::dispatch( $book_id, $booking_data );

// Code that processes an appointment booking. 

AcmeEvents::dispatch( 'booking_created', $book_id, $booking_data );

// These are identical.
AcmeEvents::dispatch( 'booking_created', [ $book_id, $booking_data ] );
AcmeEvents::dispatch( 'booking_created',  $book_id, $booking_data  );

// If we have a big-group appointment we want to send an
// email to the responsible staff to notify them in advance

// This can be replaced
if ( $appointment->participantCount() >= 5 ) {

    do_action('acme_big_group_booking_created', $appointment);

}

// With 
BigGroupBookingCreated::dispatchIf( $appointment->participantCount() >= 5, [$appointment]);

BookingCreated::dispatchUnless( $appointment->participantCount() >= 5, [$appointment]);

// Code to create an appointment object

$appointment = AppointmentCreated::dispatch([ $appointment_object ]);

// Save appointment to the Database.

// When dispatching object events the hook id is always the full class name. 

add_filter('AppointmentCreated::class', function( AppointmentCreated $event) {
    
    $appointment = $event->appointment;
    
    if ( $some_conndition === TRUE ) {
    
     // increase cost.
     $appointment->cost = 50;
    
    }
    
   return $appointment;

} );

$appointment = AcmeEvents::dispatch( 'acme_appointment_created', $appointment_object );

class MyEvent extends AcmeEvents {

    public function default($original_payload, $filtered_value) :array {
    
          return [$filtered_value];  
            
    }

}

add_filter(MyEvent::class, function ( $event ) {
    return 'foo';
});

$value = MyEvent::dispatch();

// without array typehint $value = 'foo', whoops.
$do_stuff = $value[0];

// with typehint on default()
// $value = ['foo'];


$listeners = [

    Event1::class => [
    
    // All of these class callables will work.
    Listener::class, 
    Listener::class . '@foo',
    [ Listner::class ],
    [ Listener::class, 'handleEvent' ],
    [ Listener::class, 'handleEvent' ],
    [ Listener::class, 'foobar' ],
    [ Listener::class . '@foobarbiz' ],
    [ 'custom_identifier' => Listener::class . '@foo' ],
    [ 'custom_identifier' => Listener::class],
    [ 'custom_identifier' => [ Listener::class, 'foobar' ] ],
 
    // Closures 
    function (Event1 $event ) {
    
        // Do Stuff
        
    },
    
    'custom_closure_key' => function (Event1 $event ) {
    
        // Do Stuff
        
    }
    
]

];


class BookingEventsListener {

    private $complex_mailer; 
    
    public function __construct( ComplexMailerDependency $mailer ) {

           $this->complex_mailer = $mailer;
    }

   
    public function bookingCanceled (BookingCanceled $event, BookingcomClient $bookingcom_client ) {
    
        $owner = $event->booking->owner();
        $guest = $event->booking->guest();
       
        $this->complex_mailer->confirmCancelation([ $owner, $guest ]);
        
        $bookingcom_client->updateAvailablity($event->booking_id);
        
    
    }

}

class ComplexMailerDependency {
    
    private $simple_dependency; 
    
    public function __construct( SimpleConstructorDependency $simple_dependency) {
        
       $this->simple_dependency = $simple_dependency;
       
    }

}

use BetterWpHooks\Traits\DispatchesConditionally;

class HighOrderValueCreated {

    use DispatchesConditionally;
 
    private $order; 
    
    public function __construct(Order $order ) {
    
        $this->order = $order;
        
    }
 
    public function shouldDispatch() : bool{
        
        return $this->order->total >= 500;
            
    }}

// Process appointment
AppointmentCreated::dispatch([$order]);

$listeners = [

    AppointmentCreated::class => [
    
        NotifyStaffViaSlack::class, 
        SendConfirmationEmail::class,
        AddToMailchimp::class, 
        NotifyBusinessOwner::class
    ]

];

class NotifyBusinessOwner {

    use \BetterWpHooks\Traits\ListensConditionally;

    private $sms_client;
    private $config;
 
    public function __construct( SmsClient $sms_client, Config $config ) {
    
    $this->sms_client = $sms_client;
    $this->config = $config;
    
    }
    
    public function handleEvent ( AppointmentCreated $event ) {
    
        $business_owner_phone_number = $this->config->get('primary_phone_number');
     
        $this->sms_client->notify($business_owner_phone_number, $event->appointment);
        
    }
    
    // Abstract method defined in the trait
    public function shouldHandle( AppointmentCreated $event) : bool{
    
        return $event->appointment->totalValue() >= 300;
    
    } 
}

$listeners = [

    Event1::class => [
    
        Listener1::class, 
        Listener2::class,
        Listener3::class, 
        Listener4::class
    ]

];

// getting the underlying container instance.
$container = AcmeEvents::container();

// getting the underlying dispatcher instance.
$dispatcher = AcmeEvents::dispatcher();

// These two are equivalent

#1
$dispatcher = AcmeEvents::dispatcher();
$dispatcher->hasListeners('event_id_to_look_for');

#2
AcmeEvents::hasListener('event_id_to_look_for');

AcmeEvents::listen(Event1::class, Listener::class . '@foobar');

AcmeEvents::unremovable(Event1::class, Listener::class . '@foobar');

AcmeEvents::hasListeners(Event1::class);

AcmeEvents::hasListenersFor( Listener1::class . '@foobar', Event1::class);

[ Listener1::class, '*' ]               // will search for Listener1::class + any method
[ Listener1::class . '@*']              // will search for Listener1::class + any method
[ Listener1::class . '*' ]              // will search for Listener1::class + any method
[ Listener1::class, 'foobar' ]          // will search for Listener1::class + foobar method
[ Listener1::class . '@handleEvent']    // will search for Listener1::class + handleEvent method
[ Listener1::class, 'handleEvent' ]     // will search for Listener1::class + handleEvent method
[ Listener1::class]                     // will search for Listener1::class + handleEvent method

AcmeEvents::forgetOne( Event1::class, Listener1::class . '@foobar');

// This will work.
AcmeEvents::listen( Event1::class, Listener1::class );
AcmeEvents::forgetOne( Event1::class, Listener1::class );

// This won't
AcmeEvents::listen( Event1::class, Listener1::class . '@foobar' );
AcmeEvents::forgetOne( Event1::class, Listener1::class );

// This will work.
AcmeEvents::listen( Event1::class, [ 'custom_id' => Listener1::class . '@foobar' ] );
AcmeEvents::forgetOne( Event1::class, 'custom_id' );


// This will also work with closures which is impossible with the default WordPress Plugin API
AcmeEvents::listen( Event1::class, [ 'closure_key' => function ( Event1 $event ) {

       // do stuff 

} ] );

AcmeEvents::forgetOne( Event1::class, 'closure_key' );



class OrderTest extends \PHPUnit\Framework\TestCase
{
    /**
     * Test order shipping.
     */
    public function test_orders_can_be_shipped()
    {
        // This replaces the underlying dispatcher instance with a FakeDispatcher
        AcmeEvents::fake();


        // Perform order shipping...


        // Assert that an event was dispatched...
        AcmeEvents::assertDispatched(OrderShipped::class);

        // Assert an event was dispatched twice...
        AcmeEvents::assertDispatchedTimes(OrderShipped::class, 2);

        // Assert an event was not dispatched...
        AcmeEvents::assertNotDispatched(OrderFailedToShip::class);

        // Assert that no events were dispatched...
        AcmeEvents::assertNothingDispatched();
        
    }
}

// create $order

AcmeEvents::assertDispatched(function (OrderShipped $event) use ($order) {
    return $event->order->id === $order->id;
});

// SUT
class OrderProcess {

    public function processNewOrder ($form_data) {
    
            // Do stuff with $form_data
            
            OrderCreated::dispatch([$order]);
            StockStatusUpdated::dispatch([$order]);
    
    }

}

// Registered Listeners 
$listeners = [
    
                OrderCreated::class => [
                
                    
                    // Listeners you want for integration tests
                
            ], 

                StockStatusUpdated::class => [
            
                    UpdateSlowThirdPartyApi::class,
            ]
 
        ];



// Test
class OrderProcessTest extends \BetterWpHooks\Testing\BetterWpHooksTestCase {

    protected function setUp() : void{
    
        parent::setUp(); 
        $this->setUpWp();
        
        // You need to set up your events and listeners
        $this->bootstrapAcmeEvents();
        
      
    }   

    protected function tearDown() : void{
    
        parent::tearDown();
        $this->tearDownWp();
        
    }

    public function test_orders_can_be_processed()
    {
    
        $subject = new OrderProcess();
    
        AcmeEvents::fake([
        
            StockStatusUpdated::class,
            
        ]);
        
        // All listeners for OrderCreated::class will be called. 
        // UpdateSlowThirdPartyApi::class will NOT be called. 
        $subject->processNewOrder([ // $test_data ]);
       
        // Assertions work for both events. 
        AcmeEvents::assertDispatched(OrderCreated::class);
        AcmeEvents::assertDispatched(StockStatusUpdated::class);
    
    }

}

 class CallbackCLass {
 
    public function __construct() {
        
        add_action('init', [ $this , 'doStuff']);
    
    }
 
 }

public function dispatch( $event, ...$payload ) {
			
	// Here we handle mapped events conditionally.		
	if ( ! $this->shouldDispatch( $event ) ) {
	
		return;
		
	}
	
	// Here we convert an event object so that the hook tag is the class name
	// and the payload is the actual event object
	// In a sense we just swap $event and $payload.	
	[ $event, $payload ] = $this->parseEventAndPayload( $event, $payload );
					
	// Here we handle temporary removal if a listener wants to stop
	// a listener chain.					
	$this->maybeStopPropagation( $event );
	
	// If no listeners are registered we just return the default value.	
	if ( ! $this->hasListeners( $event ) ) {
				
				
            if ( is_callable( [ $payload, 'default' ] ) ) {
            
                    return $payload->default();
            }
                        
            return is_object( $payload ) ? $payload : $payload[0];
                        
    }
		
	// If we make it this far, only here do we hit the WordPress Plugin API.
    return $this->hook_api->applyFilter( $event, $payload );
			
			
}

BookingCreated::dispatch([$booking_data]);

// Traditional Wordpress implementation.
$booking = new Booking($booking_data);

add_action('acme_booking_created', $booking );


/**
 * Wraps the created abstract listener in a closure.
 * The WordPress Hook Api will save this closure as
 * the Hook Callback and execute it at runtime.
 *
 * @param  \BetterWpHooks\Contracts\AbstractListener  $listener
 *
 * @return \Closure
 */
private function wrap( AbstractListener $listener ): Closure {
			
	// This anonymous function will be executed by Wordpress
	// Not the Listener directly.		
	return function ( $payload ) use ( $listener ) {
				
		try {
					
		     return $listener->shouldHandle( $payload ) ? $listener->execute( $payload ) : $payload;
					
		} 
		
		catch ( \Throwable $e ) {
					
		    $this->error_handler->handle($e);
					
		}
				
				
	};
}
 
if ( ! function_exists('acme_remove_filter') {
    function acme_remove_filter($tag, $callback) {
    
        AcmeEvents::forgetOne($tag, $callback);

    }
}

// Third-party dev that wants to customise AcmeEvents
acme_remove_filter(Event1::class, Listener1::class);

// This works. 
add_filter(Event1::class, ThridPartyListener::class)
woocommerce_checkout_order_processed
FALSE