kdabrow / time-machine
Laravel / Lumen library that allows to change dates in db
Installs: 6 068
Dependents: 2
Suggesters: 0
Security: 0
Stars: 5
Watchers: 2
Forks: 0
Open Issues: 0
Type:package
Requires
- php: ^7.2|^8.0
- illuminate/database: 6.0 - 12.0
Requires (Dev)
- orchestra/testbench: 4.0 - 10.0
README
Time machine
This package allows to move in time database data. It automatically selects all fields that store datetime and move them by given period or to particular date, relatively from it current value. See example to check more details.
Motivation
This package might be useful in a pre-prod environment to test log lasting processes. For example
generating customer invoice. Usually invoices are generated in a 30 days period. You might move
customer and all it's data into previous invoice cycle (30 days in to past) and effectively simulate
whole invoice cycle like that customer was created 30 days ago.
Installation
First install main package:
composer require kdabrow/time-machine
Then install database driver package:
Advice is to install those packages as --dev dependencies.
Usage
Time traveller setup
First create TimeTraveller. Only Eloquent model can be TimeTraveller. Basic configuration look like this:
<?php use Kdabrow\TimeMachine\TimeTraveller; use App\Models\User; // provide model instance $traveller = new TimeTraveller(new User()); // or class name $traveller = new TimeTraveller(User::class);
Modify query
TimeMachine by default selects all rows related to given to TimeTraveller model. You're able to provide your conditions to restrict query
<?php use Illuminate\Database\Eloquent\Builder; use App\Models\User; use App\Models\Group; use Kdabrow\TimeMachine\TimeTraveller; use Kdabrow\TimeMachine\Result; use Illuminate\Support\Arr; // Result contains all previously changed models from all TimeTravellers // For example move Users only from previously changed Group $traveller = new TimeTraveller( User::class, function(Builder $builder, Result $result) { if ($result->isSuccessful(Group::class)) { return $query->whereIn( 'group_id', $result->getSuccessful(Group::class)->pluck('id') ); } } );
Additional columns
It's possible to add additional columns into query.
<?php use App\Models\User; use Kdabrow\TimeMachine\TimeTraveller; use Kdabrow\TimeMachine\Database\Column; use Kdabrow\TimeMachine\Result; use Illuminate\Database\Eloquent\Model; $traveller = new TimeTraveller(User::class); $traveller->alsoChange('date_of_creation'); // It's possible to provide callback and determine how given field should be changed // Column object has information about currently modified column // Model is selected instance of User // Result contains previously moved in time data // In this example only move if no errors appeared during previous time travels $traveller->alsoChange( 'date_of_creation', function($currentValue, Column $column, Model $model, Result $result) { if (count($result->getAllFailed())) { return $currentValue; } return null; } );
Exclude fields
Sometimes is need to omit some fields that would usually be selected to change.
<?php use App\Models\User; use Kdabrow\TimeMachine\TimeTraveller; $traveller = new TimeTraveller(User::class); $traveller->exclude('date_of_birth');
Set up keys
Records selected to time travel are based on primary key from the model. You're able to overwrite it.
<?php use App\Models\User; use Kdabrow\TimeMachine\TimeTraveller; $traveller = new TimeTraveller(User::class); $traveller->setKeys(['uuid']);
Time machine and direction of move
After TimeTravellers are created, create TimeMachine.
<?php use Kdabrow\TimeMachine\TimeMachine; $timeMachine = new TimeMachine(); // now add previously created TimeTravellers $timeMachine->take($traveller1); $timeMachine->take($traveller2); $timeMachine->take($traveller3);
Move to the past
First create DateChooser. It is information about period or date.
<?php use Kdabrow\TimeMachine\DateChooser; use Kdabrow\TimeMachine\Result; use DateInterval; // move by DateInterval $chooser = new DateChooser(new DateInterval("P1D")); // Move by 1 day // or move by some specific amount of seconds $chooser = new DateChooser(3600); // Move by 1 hour // Now set up direction and start travel /** @var Result $result */ $result = $timeMachine ->toPast($chooser) ->start();
Move to the future
First create DateChooser. It is information about period or date.
<?php use Kdabrow\TimeMachine\DateChooser; use Kdabrow\TimeMachine\Result; use DateInterval; // move by DateInterval $chooser = new DateChooser(new DateInterval("P1D")); // Move by 1 day // or move by some specific amount of seconds $chooser = new DateChooser(3600); // Move by 1 hour // Now set up direction and start travel /** @var Result $result */ $result = $timeMachine ->toFuture($chooser) ->start();
Move to particular date
First create DateChooser. It is information about period or date.
<?php use Kdabrow\TimeMachine\DateChooser; use Kdabrow\TimeMachine\Result; use DateTime; // put DateTimeInterface object $chooser = new DateChooser(new DateTime("2020-15-16 12:12:12")); // Move datetime columns to 2020-15-16 12:12:12 // or provide datetime string $chooser = new DateChooser("2020-15-16 12:12:12"); // Move to 2020-15-16 12:12:12 // Now set up direction and start travel /** @var Result $result */ $result = $timeMachine ->toDate($chooser) ->start();
Examples
Move customer, it's payments and orders 10 days in the past
Database structure before and after change:
customers (before)
customers (after)
payments (before)
payments (after)
orders
orders
Script:
<?php use Kdabrow\TimeMachine\TimeTraveller; use Kdabrow\TimeMachine\Result; use Kdabrow\TimeMachine\TimeMachine; use App\Models\Customer; use App\Models\Payment; use App\Models\Order; use Illuminate\Support\Arr; $customerId = 100; $customerTraveller = new TimeTraveller( Customer::class, function(Builder $builder, Result $result) use ($customerId) { return $builder->where('id', '=', $customerId); } ); $customerTraveller->exclude('date_of_birth'); $paymentTraveller = new TimeTraveller( Payment::class, function(Builder $builder, Result $result) use ($customerId) { if ($result->isSuccessful(Customer::class)) { return $builder->whereIn( 'customer_id', $result->getSuccessful(Customer::class)->pluck('id') ); } } ); $orderTraveller = new TimeTraveller( Order::class, function(Builder $builder, Result $result) use ($customerId) { if ($result->isSuccessful(Payment::class)) { return $builder->whereIn( 'payment_id', $result->getSuccessful(Payment::class)->pluck('id') ); } } ); $timeMachine = new TimeMachine(); $result = $timeMachine ->take($customerTraveller) ->take($paymentTraveller) ->take($orderTraveller) ->toPast(new DateInterval("P10D")) ->start(); // Get instances that failed time travel $failed = $result->getAllFailed(); $sucessful = $result->getAllSuccessful();
Testing
Run tests from docker container
docker-compose exec php vendor/bin/phpunit
or directly from your machine
vendor/bin/phpunit