Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/meadsteve/tale
https://github.com/meadsteve/tale
Last synced: 16 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/meadsteve/tale
- Owner: meadsteve
- License: mit
- Created: 2018-06-18T19:13:37.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2019-04-21T07:08:28.000Z (over 5 years ago)
- Last Synced: 2024-10-11T03:11:12.569Z (about 1 month ago)
- Language: PHP
- Size: 45.9 KB
- Stars: 23
- Watchers: 3
- Forks: 3
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
Tale
====
[![Build Status](https://travis-ci.org/meadsteve/Tale.svg?branch=master)](https://travis-ci.org/meadsteve/Tale)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/meadsteve/Tale/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/meadsteve/Tale/?branch=master)## 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/## Installation
```bash
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.Assuming we have the following services: Flight booking API, Hotel booking API,
and a Customer API.We'd write the following steps:
```php
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)
}
``````php
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:```php
$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.## 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.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.
```php
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.## Testing / Development
Contributions are very welcome. Please open an issue first if the change is large or will
break backwards compatibility.
All builds must pass the travis tests before merge.
Running `./run_tests.sh` will run the same tests as travis.yml but locally.The dockerfile provides an environment that can execute all the tests & static analysis.