Package Data | |
---|---|
Maintainer Username: | deefour |
Maintainer Contact: | jason@deefour.me (Jason Daly) |
Package Create Date: | 2014-10-02 |
Package Last Update: | 2017-12-19 |
Language: | PHP |
License: | MIT |
Last Refreshed: | 2024-11-15 15:25:17 |
Package Statistics | |
---|---|
Total Downloads: | 38,173 |
Monthly Downloads: | 695 |
Daily Downloads: | 37 |
Total Stars: | 36 |
Total Watchers: | 6 |
Total Forks: | 4 |
Total Open Issues: | 2 |
Simple PHP Service Objects. Inspired by collectiveidea/interactor.
Run the following to add Interactor to your project's composer.json
. See Packagist for specific versions.
composer require deefour/interactor
>=PHP5.6.0
is required.
An interactor is a simple, single-purpose object.
Interactors are used to encapsulate your application's business logic. Each interactor represents one thing that your application does.
An interactor
Deefour\Interactor\Interactor
call()
methodAs a simple example, the below interactor creates a new Car
.
use Deefour\Interactor\Interactor;
class CreateCar extends Interactor
{
public function call()
{
$c = $this->context();
$c->car = new Car([ 'make' => $c->make, 'model' => $c->model ]);
if ( ! $c->car->save()) {
$this->fail('Creating the car failed!');
}
}
}
An interactor runs based on a given context. The context contains the information the interactor needs to do its work. An interactor may affect its passed context, providing data from within the interactor back to the caller.
All contexts extend the Deefour\Transformer\MutableTransformer
from the deefour/transformer
package. The MutableTransformer
provides conveient access and mutation of the underlying data, including but not limited to implementations of ArrayAccess
and JsonSerializable
.
An interactor's context can be accessed via the context()
method.
$this->context();
$this->context()->make; //=> 'Honda'
An interactor can add or modify the context.
$this->context()->car = new Car;
This can be very useful to provide data back to the caller.
Performing safe mass assignment is easy thanks to the MutableTransformer
's only()
method.
$car = new Car;
$permitted = $this->context()->only($this->car->getFillable());
$car->fill($permitted);
$car->save();
The default context constructor expects a single array of attributes as key/value pairs.
public function __construct(array $attributes = [])
{
$this->attributes = $attributes;
}
An interactor often requires specific data from the provided context. For example, a CreateCar
interactor might expect to assign an owner to the Car
it creates. A context class should be created specifically for this Interactor requiring a user be provided during instantiation.
use Deefour\Interactor\Context;
class CarContext extends Context
{
/**
* The owner of the vehicle.
*
* @var User
*/
public $user;
/**
* Constructor.
*
* @param User $user
* @param array $attributes
*/
public function __construct(User $user, array $attributes = [])
{
$this->user = $user;
parent::__construct($attributes);
}
}
The CreateCar
interactor should expect an instance of this new CarContext
during instantiation
public function __construct(CarContext $context)
{
parent::__construct($context);
}
While instantiating contexts manually is the norm, the ContextFactory
is a useful alternative in certain circumstances. Pass a fully qualified name of the context to be instantiated along with a set of attributes/parameters to the create()
method.
use App\User;
use Deefour\Interactor\ContextFactory;
$user = User::find(34);
$attributes = [ 'make' => 'Honda', 'model' => 'Accord' ];
$context = ContextFactory::create(CarContext::class, compact('user', 'attributes'));
$context->user->id; //=> 34
$context->make; //=> Honda
Explicitly specifying an 'attributes'
parameter isn't necessary. Any keys in the array of source data passed to the factory that do not match the name of a parameter on the constructor will be pushed into an $attributes
parameter. If you provide an 'attributes'
parameter manually in addition to extra data, the extra data will be merged into the $attributes
array.
Note: Taking advantage of this requires an
$attributes
parameter be available on the constructor of the context class being instantiated through the factory.
use Deefour\Interactor\ContextFactory;
$user = User::find(34);
$data = [ 'make' => 'Honda', 'model' => 'Accord' ];
$context = ContextFactory::create(CarContext::class, array_merge(compact('user'), $data));
$context->make; //=> Toyota
$context->model; //=> Accord
$context->foo; //=> bar
The state of an interactor is considered passing or failing. A context holds the current state in a $status
proprerty. This library provides a Success
and Error
status. Contexts are given a Success
status by default.
An interactor can be considered a failure when something goes wrong during it's execution. This is done by marking the context as having failed.
$this->context()->fail();
A message can be provided to explain the reason for the failure.
$this->context()->fail('Some explicit error message here');
Failing a context causes a Deefour\Interactor\Exception\Failure
exception to be thrown.
If an exception is provided it will be thrown after copying it's message over to the Error
status set on the context.
try {
$this->context()->fail(new CarCreationException('Invalid make/model combination'));
} catch (CarCreationException $e) {
(string)$this->context()->status(); //=> Invalid make/model combination
}
You can ask if the state is currently successful/passing.
$c = $this->context();
$c->ok(); //=> true
$c->status(); //=> Deefour\Interactor\Status\Success
try {
$c->fail('Oops!');
} catch (\Deefour\Interactor\Exception\Failure $e) {
$c->ok(); //=> false
$c->status(); //=> Deefour\Interactor\Status\Error
(string)$c->status(); //=> Oops!
}
Within a controller, implementing the car creation through the CreateCar
interactor might look like this.
public function create(CreateRequest $request)
{
$context = new CarContext($request->user(), $request->only('make', 'model'));
(new CreateCar($context))->call();
if ($context->ok()) {
echo 'Wow! Nice new ' . $context->car->make;
} else {
echo 'ERROR: ' . $context->status()->error();
}
}
A Deefour\Interactor\DispatchesInteractors
trait can be included in any class to reduce the creation and execution of an interactor to a single method call. In the example below, this trait will
CarContext
instance using the provided User
, 'make'
, and 'model'
CreateCar
instance with the newly created CarContext
call()
method on the interactornamespace App\Controllers;
use App\Interactors\CreateCar;
use App\Contexts\CarContext;
use Deefour\Interactor\DispatchesInteractors;
class CarController extends BaseController
{
use DispatchesInteractors;
/**
* Create a new car.
*
* @param Request $request
* @return string
*/
public function store(Request $request)
{
$context = $this->dispatchInteractor(
CreateCar::class,
CarContext::class,
array_merge([ 'user' => $request->user() ], $request->only('make', 'model'))
);
if ($context->ok()) {
return 'Wow! Nice new ' . $context->car->make;
} else {
return 'ERROR: ' . $context->status()->error();
}
}
}
Complex scenarios may require the use of multiple interactors in sequence. If a registration form asks for a user's email, password, and VIN of their car, the submission might register a new user account and create a new vehicle for the user based on the VIN. These two actions are best broken up into CreateUser
and CreateVehicle
interactors. An organizer can be used to manage the execution of these interactors.
To create an organizer, extend Deefour\Interactor\Organizer
and implement an organize()
method that pushes interactors onto the queue. The call()
method for an organizer is implemented by the library. Like a standard interactor, an organizer can require a specific context by type-hinting the constructor.
use Deefour\Interactor\Organizer;
class RegisterUser extends Organizer
{
public function __construct(RegisterUserContext $context)
{
parent::__construct($context);
}
public function organize()
{
$this->enqueue(function ($context) {
return new CreateUser(
new CreateUserContext($context->user['first_name'], $context->user['last_name'])
);
});
$this->enqueue(function ($context, $previous) {
return new CreateVehicle(
new CreateVehicleContext($previous->user, $context->vin)
);
});
}
}
The enqueue()
method accepts a callable
which should return an interactor instance. The callables are executed in a first-in-first-out manner. Each callable receives the organizer's context along with the context of the previously executed interactor.
The deferred instantiation allows for information only available after the execution of a previous interactor to be used when creating the current interactor.
An organizer is executed like any other interactor. Call the call()
method to kick things off after instantiation.
$params = [
'user' => [
'first_name' => 'Jason',
'last_name' => 'Daly',
],
'vin' => 'VINNUMBERHERE',
];
$context = new RegisterUserContext($params['user'], $params['vin']);
(new RegisterUser($context))->call();
If a failure occurs during the execution of an organizer, rollback()
will be called on each interactor that ran successfully prior to the failure, in reverse order. Override the empty rollback()
method on Deefour\Interactor\Interactor
to take advantage of this.
Note: The
rollback()
method is not called when an interactor is executed on it's own, though it can be called manually by testing for failure on the context.
A job in Laravel 5 can be treated as in interactor or organizer. The handle()
method on a job called through Laravel's job dispatcher supports dependency injection through the service container. An implementation of the CreateCar
interactor that takes advantage of dependency injection and Laravel's job dispatcher might look like this:
namespace App\Jobs;
use App\Car;
use App\Contexts\CreateCarContext as CarContext;
use Deefour\Interactor\Interactor;
use Illuminate\Contracts\Redis\Database as Redis;
class CreateCar extends Interactor
{
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(CarContext $context)
{
parent::__construct($context);
}
/**
* Execute the command.
*
* @return void
*/
public function handle(Redis $redis)
{
$c = $this->context();
$c->car = Car::create($c->only('make', 'model'));
$redis->publish('A new' . (string)$c->car . ' was just added to the lot!');
return $this->context();
}
}
Laravel needs to be told to to use it's own dispatch()
method instead of the one provided by this library. This allows both interactors and vanilla Laravel jobs can be dispatched.
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Deefour\Interactor\DispatchesInteractors;
class Controller
{
use DispatchesJobs, DispatchesInteractors {
DispatchesJobs::dispatch insteadof DispatchesInteractors;
}
}
Note: Interactors can even implement Laravel's
Illuminate\Contracts\Queue\ShouldQueue
to have execution deferred!
fail()
method. A passed exception will be thrown instead of a new Deefour\Interactor\Exception\Failure
. Thanks to @gpassarelli in #6.call()
is no longer abstract; it's left as a blank stub to be overridden. This eliminates the need to define call()
when another method is being used for business logic (ie. when using a handle()
to work with Laravel's command bus).DispatchesInteractors
trait no longer resolves interactors through the service container.
dispatch()
method has been implemented to execute the call()
method on the interactor.handle()
method, just like any other Laravel job. Constructor injection is no longer supported.
Illuminate\Foundation\Bus\DispatchesJobs::dispatch()
method when using both Laravel's job dispatcher and this library's interactor dispatcher. See Integration with Laravel above for more information.Organizer
and CompositeContext
for grouping interactors together.ContextFactory
for creating context objects.deefour/transformer
as dependency.Context
now exteds MutableTransformer
. This class no longer implements ArrayAccess
directly.attributes()
method on Context
has been removed. Use all()
or raw()
(for non-transformed version of attributes) instead.Interactor
has been simplified, using only type-hints to enforce proper context for an interactor.deefour/transformer
be required. If available, the context will be wrapped in a MutableTransformer
, providing all the functionality available in deefour/transformer
transparently on the context object.__isset()
implementation and better support for null context values.permit()
method to provide a watered down version of rails/strong_parameters whitelisting against the contents of a Context
.perform()
to call()
dispatchInteractor()
method.Copyright (c) 2017 Jason Daly (deefour). Released under the MIT License.