Git Product home page Git Product logo

tactician's Introduction

Tactician

Build Status Total Downloads MIT License

A small, flexible command bus.

IMPORTANT: You're looking at the unreleased Tactician 2.0 code. See the original docs for info about 1.x versions.

See the full docs or the examples directory to get started.

Install

Using Composer:

composer require league/tactician

Plugins

The core Tactician package is small but there are several plugin packages that extend the usefulness of Tactician:

  • PHPStan: Add static analysis support to Tactician. Highly recommended.
  • Logger: Adds PSR-3 logging support for receiving, completing or failing commands.
  • Doctrine: Wraps commands in separate Doctrine ORM transactions.
  • and many more

Framework Integration

There are a number of framework integration packages for Tactician, search for Tactician on Packagist for the most up-to-date listings.

Testing

To run all unit tests, use the locally installed PHPUnit:

$ ./vendor/bin/phpunit

Security

Tactician has no previous security disclosures and due to the nature of the project is unlikely to. However, if you're concerned you've found a security sensitive issue in Tactician or one of its related projects, please email disclosures [at] rosstuck dot com.

Contributing

Please see CONTRIBUTING for details.

tactician's People

Contributors

acairns avatar backendtea avatar big-shark avatar boekkooi avatar drowe-wayfair avatar flavioheleno avatar frankdejonge avatar grahamcampbell avatar guiwoda avatar hannesvdvreken avatar jesseobrien avatar jlaswell avatar localheinz avatar lunika avatar markredeman avatar peter279k avatar renan avatar rosstuck avatar rtuin avatar sagikazarmark avatar samnela avatar snapshotpl avatar tomaszhanc avatar wyrihaximus avatar yourwebmaker avatar zeljkablazek avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tactician's Issues

Deprecate Command interface

We originally added the Command interface when we were using decorators rather than middleware. This was useful because we can't just typehint for an object, but otherwise the interface has no real value except for arguably doing static analysis like building a list of all commands in the system. That would be a cute feature but it's not really that valuable.

I personally liked the original POPO approach to Tactician. Also, there are some coupling arguments and library integrations that've come up recently where the Command interface has been seen as a disadvantage.

Therefore, I'm going to deprecate the Command interface and replace it with an is_object check in the CommandBus instead. There's really only one Command Bus instance that'll be feasibly used here so there's little reason to do otherwise, I think.

This (along with a small change to CommandHandlerMiddleware) will probably form the basis for 0.4. When I have time to make these changes, I'll probably make PRs to all the related projects as well to start removing the interface.

If there's any major objections, we can leave it in for a release or two to provide more migration time.

CommandBusAware

I'm sure you know what I mean. It should be in the core repo, shouldn't it?

RFC: Add markdown linting for `gh_pages`

๐Ÿ‘‹ Would the maintainers be interested in PRs to improve the consistency of the markdown in the gh_pages branch for Hacktoberfest? I had worked previously with thephpleague/commonmark to introduce markdownlint Action and take passes on correcting any discovered issues (Change List).

If so, I'd add usage of DavidAnson/markdownlint (JS) as it integrates with VSCode, shell environments, as well as being used by GitHub's Super-Linter Action, further reducing the CI maintenance cost.

I created a small POC of how this would integrate with the gh_pages branch as it's different than thephpleague/commonmark's docs/ directory on main. You can review the results here.

image

Docs on the Bernard Middleware Mention Interfaces that No Longer Exist

To setup queueing on the client side, simply pass the QueueMiddleware to the Command Bus. After that you queue commands by implementing the League\Tactician\Bernard\QueueableCommand interface. (Alternatively you can implement Bernard\Message AND League\Tactician\Command) Others will be passed to the next middleware in the chain.

QueableCommand no longer exists. Neither does League\Tactician\Command.

HasCommand helper class

Previously I proposed a CommandBusAware class, now I am proposing another: HasCommand

Actually a trait.

I would use it in both command events and bernard plugin and there can be numerous use cases. Is there room for it in the core package?

Plugin documentation badges

I would rather use badges for links than the current solution.

I mean this:

Composer | [league/:package_name](https://packagist.org/packages/league/:package_name)
Github | [thephpleague/:package_name](https://github.com/thephpleague/:package_name)
Authors | [:author_name](http://twitter.com/:twitter_name)

Actually, I would use the same badges as on the main page.

CommandBus Builder

What do you think about adding a simple builder?

For example:

<?php

use League\Tactician\CommandBus;
use League\Tactician\Handler\CommandHandlerMiddleware;
use League\Tactician\Handler\CommandNameExtractor\ClassNameExtractor;
use League\Tactician\Handler\CommandNameExtractor\CommandNameExtractor;
use League\Tactician\Handler\Locator\HandlerLocator;
use League\Tactician\Handler\MethodNameInflector\HandleInflector;
use League\Tactician\Handler\MethodNameInflector\MethodNameInflector;
use League\Tactician\Middleware;

class Builder
{
    private $middlewares = [];
    private $handlerLocator;
    private $commandNameExtractor;
    private $inflector;

    public function __construct(
        HandlerLocator $handlerLocator,
        CommandNameExtractor $commandNameExtractor = null,
        MethodNameInflector $inflector = null
    ) {
        $this->handlerLocator = $handlerLocator;
        $this->commandNameExtractor = $commandNameExtractor ?: new ClassNameExtractor();
        $this->inflector = $inflector ?: new HandleInflector();
    }

    public function attachMiddleware(Middleware $middleware, $priority = null)
    {
        if ($priority !== null) {
            $this->middlewares[$priority] = $middleware;
        } else {
            $this->middlewares[] = $middleware;
        }
    }

    public function build()
    {
        ksort($this->middlewares);
        $this->middlewares[] = new CommandHandlerMiddleware(
            $this->commandNameExtractor,
            $this->handlerLocator,
            $this->inflector
        );

        return new CommandBus($this->middlewares);
    }
}

And then, we can configure CommandBus using a configuration file, for example:

<?php

$builder = new Builder(new ContainerBasedLocator($container));
$middlewares = require './middlewares.php';
foreach ($middlewares as $middleware) {
    if (is_array($middleware)) {
        if (sizeof($middleware) != 2) {
            throw new \LogicException('Invalid middleware definition.');
        }
        list($middleware, $priority) = $middleware;
    } else {
        $priority = null;
    }
    $builder->attachMiddleware($container->get($middleware), $priority);
}
$bus = $builder->build();
<?php

// middlewares.php

return [
    [LockingMiddleware::class, -255],
    [LoggingMiddleware::class, 255],
    [TransactionMiddleware::class, 0],
];

Testing with no interface

Hi, how do you approach testing with this bus?

Let's say my service has a bus as a dependency and I want to unit-test it. I'm used to create my own fake implementation instead of using eg. Mockery.

How do you do it here? The command bus doesn't have any interface. Thanks for advice! I guess people here have much more experience than me with this bus and testing code around it :)

PSR-3 logging middleware

I've started this as a decorator, needs to be ported to middleware. Also, I probably need to formalize a formatter object distinct from the middleware itself.

Make Middleware::execute internal

Nowadays I spend more time to use proper visibility everywhere, but it is not always easy.

For example in this case Middleware::execute must be public, since it is called from the outside world. However, it should not be called by the user. For these cases I would consider adding the @internal docblock.

While it is not the original purpose according to the phpDocumentor documentation, I've seen it many times (even in symfony), so I would say it is acceptable.

LockingMiddleware not running subsequent command

Looking at the LockingMiddleware source code, you didn't record the subsequent Command. Only the callable is stored. So when the $nextPending is called, the old Command is given instead. Looks like a bug to me.

I haven't actually tested this, just noticed it when I'm looking at the code.

"If a command fires off more commands"

If a command fires off more commands

This is from league/tactician-doctrine. I wonder how this is possible. It certainly is, but how? (Injecting the command bus into the handler?)

InMemoryLocator::addHandlers only allowing new instance of Handler class being stored

I was wondering what the implications of storing the mapping of a command to its handler are if this was used on a considerately sized project with many objects being initialised in an array of say 200+ entries like so:

$commandMapping = [
    // ...
    'My\CommandA' => new \My\CommandHandlerA,
    // ...
];

Would it be better if tactician allowed for the string of the class fqcn as well as the newly instantiated class? Could this have an effect on memory consumption?

Might be fine and I am worrying about nothing but thought I would ask...

Broken Examples

This commit removed the dev-dep league/container, which was used by the examples.

See the evidence below:

$ php examples/1-beginner-standard-usage.php 
PHP Fatal error:  Uncaught Error: Class "League\Container\Container" not found in /Users/flavio/Documents/open-source/tactician/examples/1-beginner-standard-usage.php:26
Stack trace:
#0 {main}
  thrown in /Users/flavio/Documents/open-source/tactician/examples/1-beginner-standard-usage.php on line 26

The solution is to either refactor the examples or add back the dev-dep.

I'm happy to provide a PR fixing this as long as the direction is pointed out.

PS: This only affects the master branch afaik.

Implement CommandBus against interface

In version 2.0-rc1 the CommandBus class is marked as final but it is not implemented against an interface.

Is there any concrete reason for not implementing the class against an interface? Could we add an interface to implement the class against?

PHP8 Support

The release date for PHP8 is in a little more than a month (check schedule here) so this issue is to track all required changes to Tactician in order to support the next major version.

I'll update it as I get things done.

Tasks:

  • Update .travis.yml to include nightly

Preventing a loop when dealing with external commands

Regarding: examples/6-conditional-handlers.php

Assuming we are using only one bus for local and remote commands, and the same codebase for our app and workers:

We send MyExternalCommand into the bus. Since our Middleware detects an implementation of ExternalCommand, we dispatch it to our external service and break the chain.

Life is good.

Our worker servers pop the job from the queue and hydrate our MyExternalCommand with the data, ready to be handled - and throw this into our bus... but the command still implements ExternalCommand, and it gets re-queued because the bus has the same middleware.

I'm trying to avoid any situation where the client determines if the command requires queueing, such as wrapping it before passed into the bus, or multiple buses. I'm happy to have it wrapped in the bus, but in this example it would still loop.

One option I'm considering is the detection of a worker environment and not including ExternalCommandMiddleware within the stack - they are supposed to do the work after all... but this feels... bad.

My code isn't exactly the same as the example, but assume ExternalCommandMiddleware gets an instance of Illuminate\Queue\QueueInterface passed in, and it's pretty much the same.

Original Twitter Conversation: https://twitter.com/andrewcairns/status/568745541087195136

Related issue: thephpleague/tactician-bernard#11

Pings: @rosstuck, @sagikazarmark (would love your thoughts guys!)

Delete the 2.0 branch

From what I can tell, master is the 2.0 branch. composer.json seems to confirm this by declaring dev-master as an alias for 2.0-dev. I think it would be less confusing if the 2.0 branch was removed.

1.0 release ?

Hello,

Thanks for your nice work on Tactician !
Do you know when you will release the 1.0 ?
What is missing to reach it ?

Yii adapter for Tactician

Hello!
At first, thank you for creating Tactician, very simple and at the same time very powerfull tool.
Just now I work with large Yii based application and I need command-bus pattern in it, so I wrote YiiTactition - simple Yii adapter for using Tactician in Yii application.

PS: sorry for my English :)

PHP version constraint tigher than required

Hi,

Your talk at the dutch php conference got me inspired to check out the command bus for our framework. Unfortunately we're stuck at PHP 5.4 at the moment and tactician requires 5.5+. I looked a bit further and I found that the 5.5 requirement is only used in the unit tests (::class syntax).

I changed the tests on my local copy and all test pass just fine in PHP 5.4. Are you interested in the PHP 5.4 testset which would allow a PHP requirement of >=5.4?

Regards,
Ron

Locking middleware does not work with exceptions

When I'm using the locking middleware, as soon as a handler throws an exception (which can happen in runtime), subsequent commands are not executed due to the isExecuting flag being true.

I'm not that familiar (yet) with the command bus in practice, but it seems it should catch exceptions, mark as not executing, and rethrow the exception.

This is especially useful when working with workers that sends multiple commands in a row.

Release v2

I have a question about ETA for release v2: When :)

Currently we have master branch which is reserved for v2, but v1 need updates to support php8. For me release v2 is ok, but to make everything smooth and ease to upgrade, create v1 branch and add support for php8 will be nice addon.

Self-installed PHPUnit doesn't work anymore

Since #48 it is impossible to run unit tests with a globally installed version of phpunit. Actually the best would be when everyone used the installed version by composer (vendor/bin/phpunit) to make sure that the right version is used.

To solve this issue I wrote a little bash script:

#!/bin/bash

PHPUNIT="$PWD/vendor/bin/phpunit"

if [ -f $PHPUNIT ]; then
    $PHPUNIT $@
else
    $HOME/.bin/phpunit.local $@
fi

It looks for a composer installed version in the given path and runs that if found. Otherwise the global one is used.

How to trigger a command from another command?

I'm just wondering if you have any best practise on how a command can trigger another command?

Injecting the command bus into a command handler causes a Stack Overflow on HHVM (which probably makes sense) so at the moment in my service class I'm setting up an event listener to fire the second command, then executing the first command which may or may not trigger the event.

Any thoughts on a more elegant way of doing this?

Laravel Service Provider

Hi I have created a working service provider for the Laravel 5, if any one interested to refer it as base for creating a better Service Provider you can find the current implementation in https://github.com/rohith-m/tactician

I am not familier with the phpunit testing so no tests are added, directory structure will need rework.

Files:

Service Provider : https://github.com/rohith-m/tactician/blob/master/src/Providers/LaravelServiceProvider.php

Locator : https://github.com/rohith-m/tactician/blob/master/src/Handler/Locator/LaravelLocator.php

Config : https://github.com/rohith-m/tactician/blob/master/src/Config/laravel.php

Hope somebody can use this info and create a better service provider for laravel

Chain Of Responsibily best practice?

Hi,

Is there any best practice example about executuning chaining command?

for example I have UserRegistrationCommand and CreateUserGroupCommand and I want execute CreateuserGroupCommand inside UserRegistrationHandler how I can do this in best way? right now I do this way below

public function handle(Command $command) 
{
    if ($this->groupRepository->isGroupExist($command->getGroupId())) {
        // Shoud I pass command bus in constructor? so I just need execure CreateUserGroupCommand
        $createUserGroupCommand = CreateUserGroupCommand();
        $createUserGroupCommand->setUserRegistrationCommand = $command;
        $this->bus->handle($createUserGroupCommand);
    }
}

This is have disadvantage, CreateUserGroupCommand always depend on UserRegistrationCommand

Is there any advice or suggestion?

[RFC] Chained Inflector

Would you be interested in adding a chained inflector?

It might be useful when you for example use 3rd-party bundle with registered command handler based on HandleInflector, but your project is configured with another inflector (e.g. InvokeInflector).

So having the chain inflector will solve this issue.

Cascading Locator

Would there be interest in a locator that iteratively attempts to use a stack of other locators, continuing to the next when the first one fails? I've been using this a lot lately, generally to have the option of assigning explicit locators when necessary and a sensible default otherwise. Example snippet:

class CascadingLocator implements HandlerLocator {

    /** @var HandleLocator[] */
    protected $locators;

    public function __construct (array $locators) {
        array_walk($locators, function($i){ if(!$i instanceof HandlerLocator) throw new NotALocatorException; });
        $this->locators = $locators;
    }

    public function getHandlerForCommand (Command $command) {
        foreach ($this->locators as $locator) {
            try {
                $handler = $locator->getHandlerForCommand($command);
            } catch (MissingHandlerException $e) {
                continue;
            }
            return $handler;
        }
        throw MissingHandlerException::forCommand($command);
    }
}

And thus usage would be wrapped:

$explicitBindings = new InMemoryLocator;
$defaultNameTransposingLocator = new SomeCustomTransposingLocator;
new CommandHandlerMiddleware($extractor, new CascadingLocator([$explicitBindings, $defaultNameTransposingLocator]), $inflector);

A proper PR would, naturally, need tests, hence this just being an Issue for now. It's really just a utility class, so I'm not sure if it deserves its own separate repository.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.