Download the PHP package douglasgreyling/light-service without Composer

On this page you can find all versions of the php package douglasgreyling/light-service. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package light-service

LightService:

Packagist Version build Actions Status Packagist License

A service object framework heavily, heavily, heavily inspired by the LightService Ruby gem.

This package ports over most of the awesome ideas in LightService so that one can use it in PHP. If you're familiar with the Ruby version, then you should feel mostly at home with this package.

Be sure to check out the original LightService if you ever find yourself in Ruby-land!

Table of Content

Why LightService?

What do you think of this code?

This controller violates the SRP. Can you imagine testing something like this?

In this instance we have a fairly simple controller, but one shudders to think what controllers could look like in more complex codebases out there in the wild.

You could argue that you could clean up this controller by moving the $tax_percentage logic and calculations into a tax model, but then you'll be relying on heavy model logic.

If you've ever done debugging (haha, who hasn't?) you might find it difficult to determine what's going on and where you need to start. This is especially difficult when you have a high level overview of what the code does and what needs to happen to resolve your bug.

Wouldn't it be nice if your code was broken up into smaller pieces which tell you exactly what they do?

In the case with our controller above, it would be great if our code dispelled any confusion by telling us that it was doing 3 simple things in a specific sequence whenever an order is updated:

  1. Looking up the tax percentage based on order total.
  2. Calculating the order tax.
  3. Providing free shipping if the total with tax is greater than \$200.

If you've ever felt the headache of fat controllers, difficult code to reason about, or seemingly endless rabbit holes, then this is where LightService comes in.

How LightService works in 60 seconds:

There are 2 key things to know about when working with LightService:

  1. Actions.
  2. Organizers.

Actions are the building blocks of getting stuff done in LightService. Actions focus on doing one thing really well. They can be executed on their own, but you'll often seem them bundled together with other actions inside Organizers.

Organizers group multiple actions together to complete some task. Organizers consist of at least one action. Organizers execute actions in a set order, one at a time. Organizers use actions to tell you the 'story' of what will happen.

Here's a diagram to understand the relationship between organizers and actions:

Getting started:

Requirements:

PHP 7.3+ is required 😅

Installation:

Your first action:

Let's make a simple greeting action.

Actions take an optional list of expected inputs and can return an optional list of promised outputs. In this case we've told our action that it expects to receive an input called name.

The executed function is the function which gets called whenever we execute/run our action. We can access the inputs available to this action through the $context variable. Likewise, we can add/set any outputs through the context as well.

Once an action is run we can access the finished context, and the status of the action.

Actions try to promote simplicity. They either succeed, or they fail, and they have very clear inputs and outputs. They generally focus on doing one thing, and because of that they can be a dream to test!

Your first organizer

Most times a simple action isn't enough. LightService lets you compose a bunch of actions into a single organizer. By bundling your simple actions into an organizer you can stitch very complicated business logic together in a manner that's very easy to reason about. Good organizers tell you a clear story!

Before we create out organizer, let's create one more action:

Now let's create our organizer like this:

And that's your first organizer! It ties two actions together through a static function call. The organizer call function takes any name and uses it to setup an initial context (this is what the with function does). The organizer then executes each of the actions on after another with the reduce function.

As your actions are executed they will add/remove to the context you initially set up.

Just like actions, organizers return the final context as their return value.

Because organizers generally run through complex business logic, and every action has the potential to cause a failure, testing an organizer is functionally equivalent to an integration test.

Simplifying our first tax example:

Let's clean up the controller we started with by using LightService.

We'll begin by looking at the controller. We want to look for distinct steps which we can separate whenever we need to update the tax on an order. By doing this we notice 3 clear processes:

  1. Look up the tax percentage based on order total.
  2. Calculate the order tax.
  3. Provide free shipping if the total with tax is greater than \$200.

The organizer:

Looking up the tax percentage:

Calculating the order tax:

Providing free shipping (where applicable):

And finally, the controller:

Tips & Tricks:

Stopping a series of actions

When nothing unexpected happens during the organizer's call, the returned context will be successful. Here is how you can check for this:

However, sometimes not everything will play out as you expect it. An external API call might not be available or some complex business logic will need to stop the processing of a series of actions. You have two options to stop the call chain:

  1. Failing the context
  2. Skipping the rest of the actions

Failing the context:

When something goes wrong in an action and you want to halt the chain, you need to call fail() on the context object. This will push the context in a failure state ($context->failure() will evalute to true). The context's fail function can take an optional message argument, this message might help describe what went wrong. In case you need to return immediately from the point of failure, you have to do that by calling next context.

In case you want to fail the context and stop the execution of the executed block, use the fail_and_return('something went wrong') function. This will immediately fail the context and cause the execute function to return.

Here's an example:

Let's imagine that in the example above the organizer could have called 4 actions. The first 2 actions were executed until the 3rd action failed, and pushed the context into a failed state and so the 4th action was skipped.

Skipping the rest of the actions

You can skip the rest of the actions by calling skip_remaining() on the context. This behaves very similarly to the above-mentioned fail mechanism, except this will not push the context into a failure state. A good use case for this is executing the first couple of actions and based on a check you might not need to execute the rest. Here is an example of how you do it:

Let's imagine that in the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd decided to skip the rest, the 4th action was not invoked. The context was successful.

Hooks

In case you need to inject code right before, after or even around actions (or even around), then hooks could be the droid you're looking for. This addition to LightService is a great way to decouple instrumentation from business logic.

Consider this code:

The logging logic makes TwoAction more complex, there is more code for logging than for business logic.

You have three options to include hooks so you can decouple instrumentation from real logic with before_each, after_each and around_each hooks:

This is how you can declaratively add before and after hooks to the organizer:

Note how the action has no logging logic after this change. Also, you can target before and after action logic for specific actions, as the $context->current_action() will have the class name of the currently processed action. In the example above, logging will occur only for TwoAction and not for OneAction or ThreeAction.

Expects and promises

The expects and promises functions are rules for the inputs/outputs of an action. expects describes what keys it needs to exist inside the context for the action to execute and finish successfully. promises makes sure the keys are in the context after the action has been executed. If either of them are violated, a custom exception is thrown.

This is how it's used:

For those who are utterly slothful, you can also set the expects and promises to a single string value if you're only dealing with one key.

Context

The context allows you to convert itself to an array:

This will convert all of the key-values inside the context to an array. Optionally you can also pass true as the first arguement to the to_array function to have the context metadata included.

The context also allows you to query metadata kept inside the context:

  1. The current action ($context->current_action();)
  2. The current organizer ($context->current_organizer();)
  3. The failure status of the context ($context->failure();)
  4. The success status of the context ($context->success();)
  5. The failure message if it exists ($context->message();)

Key aliases

The aliases property allows you to create an alias for a key found inside the organizers context. Actions can then access the context using the aliases.

This allows you to put together existing actions from different sources and have them work together without having to modify their code. Aliases will work with, or without, action expects.

If a key alias is set for a key which already exists inside the context, then an exception is raised.

Say for example you have actions AnAction and AnotherAction that you've used in previous projects. AnAction provides my_key but AnotherAction needs to use that key but expects it to be called key_alias instead. You can use them together in an organizer like so:

Error codes

You can add some more structure to your error handling by taking advantage of error codes in the context. Normally, when something goes wrong in your actions, you fail the process by setting the context to failure:

However, you might need to handle the errors coming from your action pipeline differently. Using an error code can help you check what type of expected error occurred in the organizer, or in the actions.

If this action were executed, then you can pull the error message like you would normally, but you can also retrieve the error code.

Action rollback

Sometimes your action has to undo what it did when an error occurs. Think about a chain of actions where you need to persist records in your data store in one action and you have to call an external service in the next. What happens if there is an error when you call the external service? You want to remove the records you previously saved. You can do it now with the rolled_back function.

You need to call the fail_with_rollback function to initiate a rollback for actions starting with the action where the failure was triggered.

Using the rolled_back function is optional for the actions in the chain. You shouldn't care about undoing non-persisted changes.

The actions are rolled back in reversed order from the point of failure starting with the action that triggered it.

Orchestrator logic

The Organizer - Action combination works really well for simple use cases. However, as business logic gets more complex, or when LightService is used in an ETL workflow, the code that routes the different organizers becomes very complex and imperative.

Let's look at a piece of code that does basic data transformations:

The LightService::Context is initialized with the first action, that context is passed around among organizers and actions. This code is still simpler than many out there, but it feels very imperative: it has conditionals and iterators in it.

Let's see how we could make it a bit more simpler with a declarative style:

This code is much easier to reason about, it's less noisy and it captures the goal of LightService well: simple, declarative code that's easy to understand.

The 5 different orchestrator constructs an organizer can have:

1. reduce_until

reduce_until behaves like a while loop in imperative languages, it iterates until the provided predicate in the callback function evaluates to true.

In this case the organizer above takes a number, executes a couple of actions before reducing an array of actions (in this case only containing the AddsOneAction) until the number in the context is greater than 3.

2. reduce_if

reduce_if will reduce the included actions if the predicate in the callback function evaluates to true.

In this case the organizer above takes a number, executes a couple of actions before reducing an array of actions (in this case only containing the AddsOneAction) if the number in the context is greater than 1.

A 3rd argument can be given to reduce_if which will be an array of actions to run if the predicate returns false.

3. iterate

iterate gives you iteration logic based on a string which exists as a key inside the context otherwise it will fail.

The organizer will singularize the key name and will put the actual item into the context under that name. Each element will be accessible by the singlular itme name for the actions in the iterate actions.

In this case the organizer above takes a collection of numbers and sums all the numbers together by iterating through them all.

4. execute

To take advantage of another organizer or action, you might need to tweak the context a bit. Let's say you have an array, and you need to iterate over its values in a series of actions. To alter the context and have the values assigned into a variable, you need to create a new action with 1 line of code in it.

That seems a lot of ceremony for a simple change. You can do that in an execute function like this:

In this case the organizer above simply changes the context in some way defined within the execute functions callback.

5. add_to_context

add_to_context can add key-value pairs on the fly to the context. This functionality is useful when you need a value injected into the context under a specific key right before the subsequent actions are executed.

In this case the organizer above adds some kv's into the context which the AddsOneAction needs in order to function correctly.

Context factory for faster action testing

TODO - This will come one day.

Logging

TODO - This will come one day.

Contributing

  1. Fork it
  2. Try keep your commits semantic like this.
  3. Create your feature branch (git checkout -b my-new-feature)
  4. Commit your changes (git commit -am 'fix: Added some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create new Pull Request

License

LightService is released under the MIT License.


All versions of light-service with dependencies

PHP Build Version
Package Version
Requires doctrine/inflector Version ^2.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package douglasgreyling/light-service contains the following files

Loading the files please wait ....