2017 © Pedro Peláez
 

library tale

image

mead-steve/tale

  • Thursday, June 28, 2018
  • by MeadSteve
  • Repository
  • 1 Watchers
  • 6 Stars
  • 2 Installations
  • PHP
  • 0 Dependents
  • 0 Suggesters
  • 1 Forks
  • 2 Open issues
  • 3 Versions
  • 0 % Grown

The README.md

Tale

Build Status Scrutinizer Code Quality, (*1)

What?

Tale is a small library to help write a "distributed transaction like" object across a number of services. It's loosely based on the saga pattern. A good intro is available on the couchbase blog: https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part/, (*2)

Installation

composer require mead-steve/tale

Example Usage

An example use case of this would be some holiday booking software broken down into a few services., (*3)

Assuming we have the following services: Flight booking API, Hotel booking API, and a Customer API., (*4)

We'd write the following steps:, (*5)

class DebitCustomerBalanceStep implements Step
{
    //.. Some constructor logic for initialising the api etc...

    public function execute(CustomerPurchase $state)
    {
        $paymentId = $this->customerApi->debit($state->Amount);
        return $state->markAsPaid($paymentId);
    }

    public function compensate($state): void
    {
        $this->customerApi->refundAccountForPayment($state->paymentId)
    }
class BookFlightStep implements Step
{
    //.. Some constructor logic for initialising the api etc...

    public function execute(FlightPurchase $state)
    {
        $flightsBookingRef = $this->flightApi->buildBooking(
            $state->Destination, 
            $state->Origin,
            self::RETURN,
            $this->airline
        );
        if ($flightsBookingRef=== null) {
            raise \Exception("Unable to book flights");
        }
        return $state->flightsBooked($flightsBookingRef);
    }

    public function compensate($state): void
    {
        $this->customerApi->cancelFlights($state->flightsBookingRef)
    }

and so on for any of the steps needed. Then in whatever is handling the user's request a distributed transaction can be built:, (*6)

       $transaction = (new Transaction())
            ->add(new DebitCustomerBalance($user))
            ->add(new BookFlightStep($airlineOfChoice))
            ->add(new BookHotelStep())
            ->add(new EmailCustomerDetailsOfBookingStep())

        $result = $transaction
            ->run($startingData)
            ->throwFailures()
            ->finalState();

If any step along the way fails then the compensate method on each step is called in reverse order until everything is undone., (*7)

State immutability

The current state is passed from one step to the next. This same state is also used to compensate for the transactions in the event of a failure further on in the transaction. Since this is the case it is important that implementations consider making the state immutable., (*8)

Tale provides a CloneableState interface to help with this. Any state implementing this interface will have its cloneState method called before being passed to a step ensuring that steps won't share references to the same state., (*9)

        class FakeState implements CloneableState
        {
                public function cloneState()
                {
                    return clone $this;
                }
        }

        $stepOne = new LambdaStep(
            function (MyStateExample $state) {
                $state->mutateTheState = "step one"
                return $state;
            }
        );
        $stepTwo = new LambdaStep(
            function (MyStateExample $state) {
                $state->mutateTheState = "step two"
                return $state;
            }
        );
        $transaction = (new Transaction())
            ->add($stepOne)
            ->add($stepTwo);

        $startingState = new MyStateExample();
        $finalState = $transaction->run($startingState)->finalState();

In the example above $startingState, $finalState and $state given to both function calls are all clones of each other so changing one won't affect any earlier states., (*10)

Testing / Development

Contributions are very welcome. Please open an issue first if the change is large or will break backwards compatibility., (*11)

All builds must pass the travis tests before merge. Running ./run_tests.sh will run the same tests as travis.yml but locally., (*12)

The dockerfile provides an environment that can execute all the tests & static analysis., (*13)

The Versions