Coder Social home page Coder Social logo

laravel-queueable-action's Introduction

Queueable actions in Laravel

Latest Version on Packagist GitHub Workflow Status Check & fix styling Total Downloads

Actions are a way of structuring your business logic in Laravel. This package adds easy support to make them queueable.

$myAction->onQueue()->execute();

You can specify a queue name.

$myAction->onQueue('my-favorite-queue')->execute();

Support us

We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.

We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.

Installation

You can install the package via composer:

composer require spatie/laravel-queueable-action

You can optionally publish the config file with:

php artisan vendor:publish --provider="Spatie\QueueableAction\QueueableActionServiceProvider" --tag="config"

This is the contents of the published config file:

return [
    /*
     * The job class that will be dispatched.
     * If you would like to change it and use your own job class,
     * it must extends the \Spatie\QueueableAction\ActionJob class.
     */
    'job_class' => \Spatie\QueueableAction\ActionJob::class,
];

Usage

If you want to know about the reasoning behind actions and their asynchronous usage, you should read the dedicated blog post: https://stitcher.io/blog/laravel-queueable-actions.

You can use the following Artisan command to generate queueable and synchronous action classes on the fly.

php artisan make:action MyAction [--sync]

Here's an example of queueable actions in use:

class MyAction
{
    use QueueableAction;

    public function __construct(
        OtherAction $otherAction,
        ServiceFromTheContainer $service
    ) {
        // Constructor arguments can come from the container.

        $this->otherAction = $otherAction;
        $this->service = $service;
    }

    public function execute(
        MyModel $model,
        RequestData $requestData
    ) {
        // The business logic goes here, this can be executed in an async job.
    }
}
class MyController
{
    public function store(
        MyRequest $request,
        MyModel $model,
        MyAction $action
    ) {
        $requestData = RequestData::fromRequest($myRequest);

        // Execute the action on the queue:
        $action->onQueue()->execute($model, $requestData);

        // Or right now:
        $action->execute($model, $requestData);
    }
}

The package also supports actions using the __invoke() method. This will be detected automatically. Here is an example:

class MyInvokeableAction
{
    use QueueableAction;

    public function __invoke(
        MyModel $model,
        RequestData $requestData
    ) {
        // The business logic goes here, this can be executed in an async job.
    }
}

The actions using the __invoke() method should be added to the queue the same way as explained in the examples above, by running the execute() method after the onQueue() method.

$myInvokeableAction->onQueue()->execute($model, $requestData);

Testing queued actions

The package provides some test assertions in the Spatie\QueueableAction\Testing\QueueableActionFake class. You can use them in a PhpUnit test like this:

/** @test */
public function it_queues_an_action()
{
    Queue::fake();

    (new DoSomethingAction)->onQueue()->execute();

    QueueableActionFake::assertPushed(DoSomethingAction::class);
}

Don't forget to use Queue::fake() to mock Laravel's queues before using the QueueableActionFake assertions.

The following assertions are available:

QueueableActionFake::assertPushed(string $actionClass);
QueueableActionFake::assertPushedTimes(string $actionClass, int $times = 1);
QueueableActionFake::assertNotPushed(string $actionClass);
QueueableActionFake::assertPushedWithChain(string $actionClass, array $expextedActionChain = [])
QueueableActionFake::assertPushedWithoutChain(string $actionClass)

Feel free to send a PR if you feel any of the other QueueFake assertions are missing.

Chaining actions

You can chain actions by wrapping them in the ActionJob.

Here's an example of two actions with the same arguments:

use Spatie\QueueableAction\ActionJob;

$args = [$userId, $data];

app(MyAction::class)
    ->onQueue()
    ->execute(...$args)
    ->chain([
        new ActionJob(AnotherAction::class, $args),
    ]);

The ActionJob takes the action class or instance as the first argument followed by an array of the action's own arguments.

Custom Tags

If you want to change what tags show up in Horizon for your custom actions you can override the tags() function.

class CustomTagsAction
{
    use QueueableAction;

    // ...

    public function tags() {
        return ['action', 'custom_tags'];
    }
}

Job Middleware

Middleware where action job passes through can be added by overriding the middleware() function.

class CustomTagsAction
{
    use QueueableAction;

    // ...

    public function middleware() {
        return [new RateLimited()];
    }
}

Action Backoff

If you would like to configure how many seconds Laravel should wait before retrying an action that has encountered an exception on a per-action basis, you may do so by defining a backoff property on your action class:

class BackoffAction
{
    use QueueableAction;
    
    /**
     * The number of seconds to wait before retrying the action.
     *
     * @var array<int>|int
     */
    public $backoff = 3;
}

If you require more complex logic for determining the action's backoff time, you may define a backoff method on your action class:

class BackoffAction
{
    use QueueableAction;
    
    /**
     * Calculate the number of seconds to wait before retrying the action.
     *
     */
    public function backoff(): int
    {
        return 3;
    }
}

You may easily configure "exponential" backoffs by returning an array of backoff values from the backoff method. In this example, the retry delay will be 1 second for the first retry, 5 seconds for the second retry, and 10 seconds for the third retry:

class BackoffAction
{
    /**
     * Calculate the number of seconds to wait before retrying the action.
     *
     */
    public function backoff(): array
    {
        return [1, 5, 10];
    }
}

What is the difference between actions and jobs?

In short: constructor injection allows for much more flexibility. You can read an in-depth explanation here: https://stitcher.io/blog/laravel-queueable-actions.

Testing the package

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you've found a bug regarding security please mail [email protected] instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

laravel-queueable-action's People

Contributors

adrianmrn avatar alexmanase avatar alexvanderbist avatar angeljqv avatar brendt avatar dhouweling avatar einar-hansen avatar erikn69 avatar freekmurze avatar huubvdw avatar hxnk avatar laravel-shift avatar marcmascort avatar matanyadaev avatar michielkempen avatar naoray avatar nathanheffley avatar njoguamos avatar pactode avatar patinthehat avatar pawonpawon avatar peterhollis avatar pkboom avatar rogervila avatar rubenvanassche avatar ryancco avatar shuvroroy avatar sjd-stampede avatar stancl avatar wsamoht 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

laravel-queueable-action's Issues

$tries on a chained Action is ignored

first of all, thank you for all your open source work!

the issue is basically, that the $tries property on a chained Action doesn't work. here is an example.

Action 1:

<?php

namespace App\Actions;

use Illuminate\Support\Facades\Log;
use Spatie\QueueableAction\QueueableAction;

class TestAction
{
    use QueueableAction;

    public function execute()
    {
        sleep(10);

        Log::info('TestAction done');
    }
}

The chained Action:

<?php

namespace App\Actions;

use Exception;
use Illuminate\Support\Facades\Log;
use Spatie\QueueableAction\QueueableAction;

class ChainedAction
{
    use QueueableAction;

    public int $tries = 3;

    public function backoff(): array
    {
        return [5, 30, 60];
    }

    public function execute()
    {
        sleep(10);
        throw new Exception('ChainedAction failed');
    }

    public function failed(): void
    {
        Log::error('ChainedAction failed with max tries');
    }
}

if I call the actions in a chain, the ChainedAction will not be attempted as defined per the $tries property.

$testAction = app(TestAction::class);
$chainedAction = app(ChainedAction::class);

$testAction->onQueue()
    ->execute()
    ->chain([
        new ActionJob($chainedAction),
    ]);

However, if the ChainedAction is called outside of a Chain the action will be retried normally

$chainedAction = app(ChainedAction::class);

// the action will be retried 3 times as defined by the $tries and $backoff
$chainedAction->onQueue()->execute();

I did some debugging and there seems to be some issue with how the Queueable Properties are being resolved.
tries is added dynamically to ActionJob.

protected function resolveQueueableProperties($action)
{
$queueableProperties = [
'connection',
'queue',
'chainConnection',
'chainQueue',
'delay',
'chained',
'tries',

And if I define an initial $tries property it works in ActionJob.php

public int $tries;

I am using:
V2.15
PHP 8.1
Laravel 10.43

Thanks again!

Adhere to Queueable "interface"

It would be nice if the onQueue method accepts the queue it should be dispatched to, just like Illuminate\Bus\Queueable.

Then one could use the package like this:
$notifyUsersActions->onQueue('emails')->execute();

Let me know your thoughts and I'll whip up a PR.

Adding test assertions for pushed action jobs?

As Queue::hasPushed(QueueableActionJob::class) wont work, it might be nice to include some assertions out of the box?

    public function actionJobWasPushed(string $actionJobClass): bool
    {
        $this->assertTrue(Queue::getFacadeRoot() instanceof QueueFake, 'Queue was not faked. Use `Queue::fake()`.');

        return collect(Queue::pushedJobs()[ActionJob::class] ?? [])
            ->map(function (array $queuedJob) {
                return $queuedJob['job']->displayName();
            })
            ->contains($actionJobClass);
    }

    public function assertActionJobPushed(string $actionJobClass)
    {
        $pushed = $this->actionJobWasPushed($actionJobClass);

        $this->assertTrue($pushed, "`{$actionJobClass}` was not pushed.");
    }

    public function assertActionJobNotPushed(string $actionJobClass)
    {
        $pushed = $this->actionJobWasPushed($actionJobClass);

        $this->assertFalse($pushed, "`{$actionJobClass}` was pushed.");
    }

Broken queueing actions

Hi!
When I try to queue a simple action, this error appears:

serialize(): __sleep should return an array only containing the names of instance-variables to serialize"

I saw this error in some project tests.

I'm using PHP 7.3.9, Laravel 8.28.1

Action:

class ProcessSensorDataAction
{
   use QueueableAction;

   public function execute()
   {
        dd('test');
    }
}

Controller:

    // ...
public function __invoke(Sensor $sensor)
{
    ProcessSensorDataAction::new()
        ->onQueue()
        ->execute();
 }

Is it possible to determine dynamic names to execute the actions?

Hello, people,

I have a problem with the standard defined in the public function of the shares.

In my team we define the run clause to execute a specific action. however the package only supports __invoke and execute. is there any clean way to keep using this package in this context?

  • The action execution method
public function run(
        ContractData $contractData
    ) : void { ... }
  • Execution in queue
$action->onQueue()->run($contractData)
  • Error
at vendor/spatie/laravel-queueable-action/src/ActionJob.php:94
     9091public function handle()
     92▕     {
     93$action = app($this->actionClass);
  ➜  94$action->{$action->queueMethod()}(...$this->parameters);
     95▕     }
     9697public function __sleep()
     98▕     {

      +20 vendor frames 

      Illuminate\Foundation\Bus\PendingDispatch::__destruct()

in this case it is run but it would be ideal to be able to leave open the possibility of using any name (execute, run, notificate, generate, ... ).

Does anyone have an idea how to fix this ?

Allow extending the `ActionJob` class or use a custom one via configuration

It would be super useful to allow using a custom ActionJob class via configuration. Something like this:

return [
    'model' => \Spatie\QueueableAction\ActionJob::class
];

I have a use case where I need to track the status of my jobs (progression, etc). I'm trying to use a package called Laravel Job Status which requires me to add a trait to the dispatched jobs. I still have not found a clean way to do that. So it will be a great help to have control over the ActionJob class.

I can submit a PR for this feature.

Problem when serializing a model with binary data

Hello!

First of all: Thanks for the practical package and the great work!

I have a problem that I couldn't solve myself so far.
I have several models that have binary data (performant UUIDs).

If I want to push an ActionJob onto the queue, that has such a model as a parameter, the serialization doesn't work correctly. All models stored in the ActionJob in $parameters will not be serialized as expected with the toArray() function of the models and so it comes to an error Unable to JSON encode payload. Error code: 5

Maybe you have a clue or some help for me?
Thanks a lot!

Accessing model properties in queued Action

I have a model with custom attribute:

    protected function currentRate(): Attribute
    {
        $rate = Cache::remember("route:{$this->id}:current_rate:{$this->updated_at->timestamp}", 3600, function () {
            $rateCurrent = $this->rates()->current()->first();
            return $rateCurrent;
        });

        return Attribute::make(
            get: fn ($value) => $rate
        );
    }

When accessing this attribute through Command or Job (queued or sync) it works fine:

sag@Sag-MacBook-Pro-15 zz_api % php artisan command:name
1
2
10
4
5
6
7
8
9

When accessing this attribute through queued Action result is:

[2022-07-10 19:03:27] local.ERROR: Attempt to read property "timestamp" on null {"exception":"[object] (ErrorException(code: 0): Attempt to read property \"timestamp\" on null at /Users/sag/Documents/code/zz_api/app/BulkSms/Models/Routes/Route.php:38)

Expected behaviour is to return value as expected, especially that the tests also complete OK.

Passing execute parameters to middleware method

I currently have an action where im setting up middleware. The middleware requires a parameter, but currently it's not possible to achieve this. Using laravel jobs, it is possible because the parameters you pass are put in the constructor, whereas actions use the execute method. When trying to achieve the same using actions, it's not possible. You don't have access to the model.

This wont work

public function execute($model)
{
    // Model is available here
}

public function middleware(): array
{
    // Cannot access the model here
    return [(new WithoutOverlapping($this->model->id))];
}

Proposed solution:
If we pass the same parameters for execute to the middleware, we will have access to them. We can do that here.

Making it like this

if (is_object($action))
{
    $this->tags = $action->tags(...$parameters);
    $this->middleware = $action->middleware(...$parameters);
    if (method_exists($action, 'backoff')) {
        $this->backoff = $action->backoff(...$parameters);
    }
    if (method_exists($action, 'failed')) {
        $this->onFailCallback = [$action, 'failed'];
    }
}

Now we have access to the parameter like this:

public function middleware(Model $model): array
{
    return [(new WithoutOverlapping($model->id))];
}

If we do this, we also have to delete these lines. So that will be a breaking change.

Let me know what you think. And if this is also something that you'd like, I can open a PR for it.

Tries/timeout isn't processed

$args = [$model];

app(SetMetadata::class)
            ->onQueue('optimize')
            ->execute(...$args)
            ->chain([
                new ActionJob(CreateThumbnail::class, $args),
                new ActionJob(CreatePreview::class, $args),
                new ActionJob(CreateSprites::class, $args),
            ]);

Results in Trying to get property 'tries' of non-object when setting timeout and/or tries in the action job class:

class CreatePreview
{
    use QueueableAction;

    /**
     * @var int
     */
    public $tries = 3;

    /**
     * @var int
     */
    public $timeout = 300;
}

What I'm I doing wrong? Thanks!

Syntax error on ver. 2.10.3 - PHP 7.3.33

PHP Unit test failed because syntax error. I think the arrow function only available on 7.4
But the composer.json itself, have min requirement of PHP ^7.2.
I installed it on PHP 7.3

Is this a bug?

syntax error, unexpected '=>' (T_DOUBLE_ARROW), expecting ')'

  at vendor/spatie/laravel-queueable-action/src/Testing/QueueableActionFake.php:94
     90▕
     91▕     protected static function getChainedClasses()
     92▕     {
     93▕         return collect(Queue::pushedJobs()[ActionJob::class] ?? [])
  ➜  94▕             ->map(fn ($actionJob) => $actionJob['job']->chained)
     95▕             ->map(function ($chain) {
     96▕                 return collect($chain)->map(function ($job) {
     97▕                     return unserialize($job)->displayName();
     98▕                 });

https://github.com/spatie/laravel-queueable-action/blob/2.10.3/src/Testing/QueueableActionFake.php#L94

Add support for failed method

Laravel's jobs support method failed. It is a very convenient way to process failed jobs.
It will be great to add support for this method

And there is no way to replace ActionJob in QueueableAction. So I cannot use my realization of ActionJob with failed method without overwriting QueueableAction

Running `composer test` fails

As soon as I run composer test or php vendor/bin/pest I get the following error(s):

could not find driver (Connection: testing, SQL: PRAGMA foreign_keys = OFF;)

Am I doing something wrong or is there something wrong inside you test-case?

Failed-method may cause serialization issues

In a nutshell, defining a failed method in the action may cause serialization issues like Serialization of 'Closure' is not allowed..

Minimal reproduction would look something like this (this assumes that PSR-17 & PSR-18 factories / clients are bound to DI container, in this case I was using Guzzle):

// the action
<?php

namespace App\Actions;

use Illuminate\Support\Facades\Log;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriInterface;
use Spatie\QueueableAction\QueueableAction;
use Throwable;

final class TestAction
{
    use QueueableAction;

    public function __construct(
        private ClientInterface $httpClient,
        private RequestFactoryInterface $requestFactory,
        private StreamFactoryInterface $streamFactory,
    ) {
    }

    public function execute(UriInterface $uri, array $payload): void
    {
        $request = $this->requestFactory->createRequest('POST', $uri)
            ->withHeader('Content-Type', 'application/json')
            ->withBody($this->streamFactory->createStream(json_encode($payload)));
        $this->httpClient->sendRequest($request);
    }

    public function failed(Throwable $exception): void
    {
        // Send user notification of failure, etc...
        Log::debug('TestAction failed', ['exception' => $exception->getMessage()]);
    }
}


// Calling code; tinker works
$uri = resolve(Psr\Http\Message\UriFactoryInterface::class)->createUri('http://example.com/');
$payload = ['foo' => 'bar'];
resolve(App\Actions\TestAction::class)->onQueue()->execute($uri, $payload);
// which throws;  Exception  Serialization of 'Closure' is not allowed.

ActionJob sets the failure handler in the constructor like this;

if (method_exists($action, 'failed')) {
    $this->onFailCallback = [$action, 'failed'];
}

This will cause the serialization of the whole action-instance, which in turn causes the actual error if any of the instance properties are not serializable.

I'm not sure if this constitutes as an issue of the library code as the error is caused by the user-defined action, but this is something that could potentially be handled in the library as well. I understand that serializing the actual instance has some benefits like preserving the actual instance properties, but given that those are more or less "static" (you are not really supposed to use constructor to do that much besides DI I think) the execute method parameters are probably the more important ones anyway. In that case the failure handler could actually be refactored slightly.

  1. Don't set the onFailCallback property at all in the ActionJob
  2. Change the failed-handler in the ActionJob;
     public function failed(Throwable $exception)
     {
         if (method_exists($this->actionClass, 'failed')) {
            // `execute` parameters here could be handy when debugging
             return resolve($this->actionClass)->failed($exception, $this->parameters);
         }
     }
    

As stated above I'm not that sure if this is an issue or not, but at least this may give some ideas to anyone else experiencing this.

Ps. thanks for your great work on open-source packages. 🙌

Pps. In case this is not deemed to be a library-issue (feature?), this should be fixed in the action code by gracefully handling the serialization / deserialaization with __sleep & __wakeup handlers.

Determine if execute() was called from queue or not

Hello!

I've got case with executing some extra code, when action is called from queue:

if ($shouldBequeued) {
    // In this case i'd like to send some notifications to front in order to show end user status of execution
    $action->onQueue()->execute();
} else {
    $action->execute();
}

Is there any opportunity to determine if my action is run from queue? If not, maybe any hint? 🙄

Thanks in advance

Call to a member function chain() on null

I am unsure on even asking this question because it's probably so simple but cannot get a working example. The code below gives me the error Call to a member function chain() on null. The examples don't show any actions returning anything and I cannot find examples anywhere else so I am unsure how to proceed or how to debug.

$layout = 'some relevant string data';
$args = [
      'user' => $user,
      'product' => $product
    ];
    app(GeneratePdfAction::class)
      ->execute($layout, $args)
      ->chain([
        new ActionJob(StoreFileAction::class, $args),
      ]);

I'd like to use this package to pass data from GeneratePdfAction to StoreFileAction. Is it intended be used in this way? I read somewhere you can pass data from one action to the other within a chain, but I don't see an example of that here either.

Actions invocation style

While actions are very powerful, the current invocation style is quite messy, and out of the laravel coding style.

// nested of this
$myInvokeableAction->onQueue()->execute($model, $requestData);

// why not to
MyInvokeableAction::execute($model, $requestData)->onQueue();

Just like we dispatch jobs in laravel? it is related to the IDE auto completion or there is another benefit?

Execute method is hardcoded without abstract/interface

The method to be executed is hardcoded but there is no Action abstract class or interface which forces the user to define a execute() method.
For me it would be even better if the method name isn't enforced because laravel standard is handle() and __invoke() could also be great. So how about using __call() and pass the called method name to the queue job?
This way the user could name the executing method how ever he wants.

Add laravel common make() method to an abstract action

It would be great to have a Laravel common make() method. Right now I've my own abstract base action but I think that this could also fit in this package.

abstract class AbstractAction
{
    public static function make(array $parameters = []): self
    {
        return app(static::class, $parameters);
    }
}

What do you think? If you agree I'm happy to draft a PR.

Adding properties to queueable actions

I'm currently facing a problem where I'm working with a multi-tenant multi-database setup in a project where I'd like to pass along the Customer (id) to the queueable action to ensure the action can later on resolve data from the correct database.

The problem that I'm facing is that the data I pass along to the execute function can't be unserialized because the table and/or data is not present in the main database, which is a logical thing with multi-tenant multi-database.

Therefore I want to add the id of the Customer(the model which controls the tenant behaviour) so the action/job so that I can connect to correct database before unserializing the other data.

This is my current (simplified) setup:

BaseAction:

<?php

namespace App\Actions;

/** @method execute */
abstract class BaseAction
{
    public static function make(): static
    {
        return new static();
    }
}

BaseQueuedAction:

<?php

namespace App\Actions;

use Spatie\QueueableAction\QueueableAction;

abstract class BaseQueuedAction extends BaseAction
{
    use QueueableAction;

    ppublic function onQueue(?string $queue = null, ?int $customerId = null)
    {
        /** @var self $class */
        $class = new class($this, $customerId, $queue)
        {
            protected $action;

            protected $queue;

            protected $customerId;

            public function __construct(object $action, ?int $customerId, ?string $queue)
            {
                $this->action = $action;
                $this->customerId = $customerId;
                $this->onQueue($queue);
            }

            public function execute(...$parameters)
            {
                $actionJobClass = $this->determineActionJobClass();

                return dispatch(new $actionJobClass($this->action, $this->customerId, $parameters))
                    ->onQueue($this->queue);
            }

            protected function onQueue(?string $queue): void
            {
                if (is_string($queue)) {
                    $this->queue = $queue;

                    return;
                }

                if (isset($this->action->queue)) {
                    $this->queue = $this->action->queue;
                }
            }

            protected function determineActionJobClass(): string
            {
                $actionJobClass = config('queuableaction.job_class') ?? ActionJob::class;

                if (!is_a($actionJobClass, ActionJob::class, true)) {
                    throw InvalidConfiguration::jobClassIsNotValid($actionJobClass);
                }

                return $actionJobClass;
            }
        };

        return $class;
    }
}

ActionJob:

<?php

namespace App\Jobs;

use Exception;
use App\Models\Customer;
use Spatie\QueueableAction\ActionJob as SpatieActionJob;

class ActionJob extends SpatieActionJob
{
    protected ?int $customerId;

    public function __construct(
        $action,
        ?int $customerId = null,
        array $parameters = [],
    )
    {
        $this->customerId = $customerId;
        parent::__construct($action, $parameters);
    }

    public function __unserialize(array $values)
    {
        info('Customer id: ' . $this->customerId ?? 'not set');

        if ($this->customerId) {
            $this->connectTenantDatabase();
        }

        return parent::__unserialize($values);
    }

    /** @throws Exception */
    protected function getCustomer(): Customer
    {
        if (!$this->customerId) {
            throw new Exception('Customer ID not set');
        }

        return Customer::findOrFail($this->customerId);
    }

    /** @throws Exception */
    protected function connectTenantDatabase(): void
    {
        info('customer id for connecting to tenant database ' . $this->customerId);
        $customer = $this->getCustomer();

        $customer->connectDB();
    }
}

GenerateReportSnapshot:

<?php

namespace App\Actions;

class GenerateReportSnapshotAction extends BaseQueuedAction {
    public function execute(Report $report): void {
        info('Report to create snapshot for: ', [$report->getKey()]);
    }
}

TestController:

<?php

namespace App\Http\Controllers;

use App\Models\Customer;
use App\Models\Report;
use App\Models\User;
use App\Actions\GenerateReportSnapshotAction;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class TestController extends Controller {
    public function test(Request $request, GenerateReportSnapshotAction $action): JsonResponse {
        /** @var User $user */
        $user = $request->user();

        /** @var Customer $customer */
        $customer = $user->customer;

        /** @var Report $report */
        $report = Report::firstOrFail();

        $action->onQueue(null, $customer->getKey())
            ->execute($report);

        return response()
            ->json([
                'success' => true,
           ]);
    }
}

The problem is that while having thecustomerId passed along to the (queued)action by overriding the onQueue method with an additional parameter and creating a custom ActionJob, during the __unserialize function the value of $this->customerId always is null. My log states:

local.ERROR: Typed property App\Jobs\ActionJob::$customerId must not be accessed before initialization {"exception":"[object] (Error(code: 0): Typed property App\\Jobs\\ActionJob::$customerId must not be accessed before initialization at /Users/rickgoemans/Code/project/app/Jobs/ActionJob.php:25)
[stacktrace]
#0 [internal function]: App\\Jobs\\ActionJob->__unserialize(Array)

Any help would be greatly appreciated because I'm at this for like 5 hours already.

EDITS: corrected some simplified code

Models aren't (de)serialized as expected when executing action on queue

The queued ActionJob does use the SerializesModels trait, but since the Action parameters are stored inside the $parameters array property, any Eloquent Models inside it will not get (de)serialized as expected.
The trait will not look inside the array, and just serialize the array as is.

Besides being inefficiënt, this also leads to stale data, in case passed models change between dispatching and handling.

A possible solution would be to override the methods responsible for (de)serialization, by manually serializing the $parameteres array. This is how I solved it for now, but maybe there's a better solution:

class ActionJob implements ShouldQueue
{
    use SerializesModels {
        __sleep as serializesModelsSleep;
        __wakeup as serializesModelsWakeup;
        __serialize as serializesModelsSerialize;
        __unserialize as serializesModelsUnserialize;
    }

    // ...

    public function __sleep()
    {
        foreach ($this->parameters as $index => $parameter) {
            $this->parameters[$index] = $this->getSerializedPropertyValue($parameter);
        }

        $this->serializesModelsSleep();
    }

    public function __wakeup()
    {
        $this->serializesModelsWakeup();

        foreach ($this->parameters as $index => $parameter) {
            $this->parameters[$index] = $this->getRestoredPropertyValue($parameter);
        }
    }

    public function __serialize()
    {
        foreach ($this->parameters as $index => $parameter) {
            $this->parameters[$index] = $this->getSerializedPropertyValue($parameter);
        }

        return $this->serializesModelsSerialize();
    }

    public function __unserialize(array $values)
    {
        $this->serializesModelsUnserialize($values);

        foreach ($this->parameters as $index => $parameter) {
            $this->parameters[$index] = $this->getRestoredPropertyValue($parameter);
        }

        return $values;
    }
}

Action batching

Laravel 8 introduced Job Batching, I think it's necessary to make this package support action batching in order to remain up-to-date.

A workaround is to use directly the ActionJob, but I think it should have a better built-in support.

Bus::batch([
  new ActionJob(SimpleAction::class),
  new ActionJob(SimpleAction::class),
  new ActionJob(SimpleAction::class),
])->withFailures()->dispatch();

I have no problem to implement it. I just need your help finding an ideal design for this feature.

Error in Package Discover

Hi, first of all i appreciate all your packages, thank you for all

Then, after installing the this package, i`ve got a error
capturar

seeing the composer.json and searching for providers, you cannot find this mentioned class, but in the first release 0.0.1 the composer as diferent than master branch and had this class in providers, then getting this error.

Normal QueueableAction class vs Trait

Since the current implementation doesn't support IDE autocomplete and other things, why not implementing the QueueableAction as a class instead of a trait, and it could be extended when the action is needed to be queueable.
Example:

class GeneralAction extends QueueableAction
{
    public function __invoke()
    {
         //do something
    }
}

I'm open to doing the PR for those changes if the idea is welcomed

Add ability to add custom tags

Maybe i missed it but would it be possible to allow adding additional tags for horizon and monitoring the queueable actions?

add support for ShouldQueue interface

The ShouldQueue interface is a laravel common to tell the queueable class to queue itself as normal behavior.
It would be great if this packages supports this interface and queues actions called via execute() only if the class implements this interface.

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.