---
description: >-
  Describes the recommended workflow in steps in order to build a feature with
  the most efficient use of time, showcasing how Lucid enhances productivity and
  efficiency.
---

# Workflow

Building applications is as similar in concept as building any artefact made of multiple pieces such as houses, towers, castles etc. which are all built on the basis of the same construct - stone walls. For that we need material, structure, measurements and most importantly sequence; for we need the base layer of stones and cement to build upon, and it is built row by row.

In this guide we show the recommended process of building robust (yet flexible) blocks that make our application whole, ensuring ideal levels of productivity and efficiency by outlining the composition and interaction of our components prior to indulging into implementation - [**Thinking time**](https://books.google.com.ua/books?id=TVQUAAAAQBAJ\&lpg=PA11\&ots=O_P4gAod-X\&dq=thinking%20time%20peopleware\&hl=uk\&pg=PA11#v=onepage\&q\&f=false) as described in [Peopleware](https://en.wikipedia.org/wiki/Peopleware:_Productive_Projects_and_Teams) is the objective of this workflow.

## Writing Features

1. Generate a Feature class, which will create its test class along.
2. Open the Feature class and describe the steps to achieve the feature's functionality in `TODO` items such as:

```php
 class UpdateResourceFeature extends Feature
 {
     public function handle(Request $request)
     {
         // 1. @TODO: validate request input (format & length)

         // 2. @TODO: verify access to resource

         // 3. @TODO: update resource

         // 4. @TODO: save audit activity (async)

         // 5. @TODO: notify followers (async)

         // 6. @TODO: respond with update status
     }
 }
```

This approach gives the benefit of an organised and defined workflow, it enhances productivity, focus and visibility by summarising the steps as an overview of what it is to be implemented. In addition to the benefit of editors listing those when needed:

![To Do list based on comments.](https://3915797027-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2Fdocs%2F-M5HRWcuss7dCK5-EFKs%2F-M5HRen_IOdeCpjrKnL8%2FScreenshot_2020-03-21_at_12.59.13_PM.png?generation=1587299922868526\&alt=media)

1. Start by writing the test for the feature: it is a request to call the route with one of the acceptance cases with input params (if applicable) to execute this feature. At this step, we will have to create the route, controller and controller method to serve the feature, and add the `serve(UpdateResourceFeature::class)` line to the controller method.
2. Now that we've written our first test case to execute the feature, it sure will fail because our Feature class is still empty. Now, we can start filling this feature with jobs by replacing each of these steps with the Job or Operation class that fulfils it, following the workflows below.

## Writing Operations

Similar to Features, Operations group multiple Jobs that are often executed together, or their combination form a single piece of functionality that itself can be split into multiple reusable steps. However, they differ in testing.

1. Generate an Operation class, which will create its test class along.
2. Open the Operation class and describe the steps that implement the functionality in comments

```php
 class NotifyFollowersOperation extends Operation
 {
     public function handle(Request $request)
     {
         // 1. @TODO: query followers IDs (paginate)

         // 2. @TODO: enqueue notifications in batches of 50

         // 3. @TODO: return status
     }
 }
```

1. Start writing the test for the operation: It is a unit test to ensure one thing:

> ### That it runs the correct jobs with the correct parameters, in the correct order.

Jobs don't really run within an Operation test, we rely on the unit test of the single job to ensure its integrity, while operations are about running multiple jobs in sequence. We achieve this by [partially mocking](http://docs.mockery.io/en/latest/reference/partial_mocks.html) the Operation class.

```php
 $mOperation = Mockery::mock(NotifyFollowersOperation::class)->makePartial();
```

And then we ensure it runs and returns as expected:

```php
 $mOperation->shouldReceive('run')
     ->with(GetFollowersJob::class, ['userId' => $userId, 'fields' => 'id'])
     ->once()->andReturn($result);
```

With this we have drawn a map of how we would like our codebase to be composed on the outside, and what is expected from each component for this implementation. It is now that we start detecting reusable components and we adjust as desired. For example we might figure that a similar functionality is already implemented and needs a simple adjustment to be used in the current operation or feature, so we go ahead and include a `@TODO` item to update it.

Of course, this is an iterative process where we rarely get things right from the first time so feel free to draft as many possibilities until you are satisfied with the final result, then start filling Job classes with implementation.

At this stage we would have had to create our Job classes so that we cloud use them in the test of our Operation and Feature classes, next will be to fill those Jobs with implementation.

## Writing Jobs

Resulting from our previous steps of the workflow, a list of tasks that we can start ticking off and our feature will be implemented completely by the end. All that is left is to start filling Job classes with implementations.

1. If you hadn't generated the Job class previously, generate a Job class for each of the items previously marked as a "to do" item. It is usually best to generate one class at a time, fill it, ensure that it works as expected and that it provides the functionality required and then move on to the next. As for the sequence of generating the jobs, it is usually best to follow the sequence in the Feature class.
2. Open the generated Job class and start writing functionality. If you are into TDD, you may prefer to open the test file and write your expectations first. Nevertheless, it is not important nor does it affect the outcome to whether you write the test or the implementation first; whichever suites you best.

## Demo

Let's exercise this workflow with a demo. If you prefer to read the resulting code you can find it on [https://github.com/lucid-architecture/demo-workflow](https://github.com/lucid-architecture/demo-workflow)

To simplify this exercise, we will be working with a [Lucid Microservice](https://github.com/lucid-architecture/laravel-microservice) installation.

Create project

```bash
 composer create-project lucid-arch/laravel-microservice workflow-demo
```

Generate Feature

```
lucid make:feature UpdateArticle
```

Fill the Feature preliminarily to know what steps would fulfil its functionality

{% code title="app/Features/UpdateArticleFeature.php" %}
```php
namespace App\Features;

use Lucid\Foundation\Feature;
use Illuminate\Http\Request;

class UpdateArticleFeature extends Feature
{
    public function handle(Request $request)
    {
        // 1. @TODO: validate request input (format & length)

        // 2. @TODO: verify access to resource

        // 3. @TODO: update resource

        // 4. @TODO: save audit activity (async)

        // 5. @TODO: notify followers (async)

        // 6. @TODO: respond with update status
    }
}
```
{% endcode %}

And now we tackle these jobs and operations one by one.

### Step 1 - Validate Input

`// 1. @TODO: validate request input (format & length)`

Generate the class with `lucid make:job ValidateArticleInput Article` and fill it with the expected content

No we write the code and the test for our first job.

{% hint style="info" %}
Switch between Code and Test in the tabs below. This applies to all classes with tests.
{% endhint %}

{% tabs %}
{% tab title="Code" %}
{% code title="app/Domains/Article/Jobs/ValidateArticleInputJob.php" %}
```php
namespace App\Domains\Article\Jobs;

use Lucid\Foundation\Job;
use Lucid\Foundation\Validator;

class ValidateArticleInputJob extends Job
{
    /**
     * @var array
     */
    private $input;

    /**
     * @var array
     */
    private $rules = [
        'title' => ['required', 'string', 'max:100'],
        'content' => ['required', 'string', 'max:1000'],
    ];

    /**
     * Create a new job instance.
     * @param array $input
     */
    public function __construct(array $input)
    {
        $this->input = $input;
    }

    /**
     * Execute the job.
     *
     * @param Validator $validator
     * @return bool
     *
     * @throws \Lucid\Foundation\InvalidInputException
     */
    public function handle(Validator $validator) : bool
    {
        return $validator->validate($this->input, $this->rules);
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Domains/Article/Jobs/ValidateArticleInputJob.php" %}
```php
namespace App\Domains\Article\Tests\Jobs;

use App\Domains\Article\Jobs\ValidateArticleInputJob;
use Lucid\Foundation\Validator;
use Tests\TestCase;

class ValidateArticleInptJobTest extends TestCase
{
    public function test_validate_article_input_job()
    {
        $job = new ValidateArticleInputJob([
            'title' => 'The Title',
            'content' => 'The content of the article goes here.',
        ]);

        $this->assertTrue($job->handle(app(Validator::class)));
    }

    /**
     * @dataProvider articleInputValidationProvider
     * @expectedException \Lucid\Foundation\InvalidInputException
     */
    public function test_validating_article_input_job_rules($title = null, $content = null)
    {
        $job = new ValidateArticleInputJob(compact('title', 'content'));
        $job->handle(app(Validator::class));
    }

    public function articleInputValidationProvider()
    {
        return [
            'without title' => [
                'content' => 'The content of the article.',
            ],
            'title is empty' => [
                'title' => '',
                'content' => 'The content of the article.',
            ],
            'without content' => [
                'title' => 'The Title Only',
            ],
            'content is empty' => [
                'title' => 'The Title Here',
                'content' => '',
            ],
            'max title length' => [
                'title' => str_repeat('a', 101),
                'content' => 'Content goes here',
            ],
            'max content length' => [
                'title' => 'Title here',
                'content' => str_repeat('a', 1001),
            ],
        ];
    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

Running the tests now will yield the following results

```php
PHPUnit 7.5.8 by Sebastian Bergmann and contributors.

.......I..                                                        10 / 10 (100%)

Time: 351 ms, Memory: 18.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 10, Assertions: 9, Incomplete: 1.
```

I know, the incomplete test is annoying, but it is there for a purpose! This is the Feature test, telling us that we're not done yet. Lucid test files are initially generated with a `markTestIncomplete` for that purpose.

### Step 2 - Authorise Access

`// 2. @TODO: verify access to resource`

Generate Job with `lucid make:job AuthoriseArticleUpdateJob Article`

This job will require some setup before we can dive into it since it deals with database. We will use SQLite for testing, which will work as-is with any other SQL database, thanks to [Eloquent](https://laravel.com/docs/5.4/eloquent).

Update `phpunit.xml` to run the tests in an SQLite memory instance

```markup
...
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
    </php>
</phpunit>
```

A fix is required for factories to work, go to `database/factories/ModelFactory.php` and change the namespace of `User::class` to `App\Data\Models\User::class`

Generate a migration to create our Article model's table

```php
php artisan make:migration create_articles_table
```

Fill the migration as follows

```php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateArticlesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->timestamps();
            $table->string('title');
            $table->text('content');
            $table->unsignedBigInteger('user_id');

            $table->foreign('user_id')
                ->references('id')->on('users')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('articles');
    }
}
```

Now we can create our `Article` model. Create `app/Data/Models/Article.php`

{% code title="app/Data/Models/Article.php" %}
```php
namespace App\Data\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected $fillable = ['title', 'content'];

    public function user()
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}
```
{% endcode %}

Add the relationship `User <> Article`

```php
public function articles()
{
    return $this->hasMany(Article::class, 'user_id');
}
```

Create `Article` factory to help us with our tests

{% code title="database/factories/ArticleFactory.php" %}
```php
$factory->define(App\Data\Models\Article::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->realText,
        'user_id' => function () {
            return factory(App\Data\Models\User::class)->create()->id;
        },
    ];
});
```
{% endcode %}

Now we are ready to start writing our job.

This job simply fetches the user with the intended article as a relationship with a `firstOrFail` which will throw a `ModelNotFoundException` if the record is not found, indicating that either one of the records doesn't exist (user or article) or the relationship between them doesn't exist, meaning that the intended article doesn't belong to this user. Then we write the test to ensure this functionality serves as intended.

{% tabs %}
{% tab title="Code" %}
{% code title="app/Domains/Article/Jobs/AuthoriseArticleUpdateJob.php" %}
```php
namespace App\Domains\Article\Jobs;

use Lucid\Foundation\Job;
use App\Data\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class AuthoriseArticleUpdateJob extends Job
{
    /**
     * @var User
     */
    private $user;
    /**
     * @var int
     */
    private $articleId;

    /**
     * Create a new job instance.
     *
     * @param User $user
     * @param int $articleId
     */
    public function __construct(User $user, int $articleId)
    {
        $this->user = $user;
        $this->articleId = $articleId;
    }

    /**
     * Execute the job.
     *
     * @throws ModelNotFoundException
     */
    public function handle()
    {
        // only the owner of the article is authorised to update it.
        User::where('id', $this->user->id)
                    ->whereHas('articles', function ($query) {
            $query->where('id', $this->articleId);
        })->firstOrFail();
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Domains/Article/Jobs/AuthoriseArticleUpdateJonTest.php" %}
```php
namespace App\Domains\Article\Tests\Jobs;

use App\Data\Models\Article;
use App\Data\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use App\Domains\Article\Jobs\AuthoriseArticleUpdateJob;
use Tests\TestCase;

class AuthoriseArticleUpdateJobTest extends TestCase
{
    use RefreshDatabase;
    use DatabaseMigrations;

    public function test_authorise_article_update_job()
    {
        $article = factory(Article::class)->create();

        $user = $article->user;

        $job = new AuthoriseArticleUpdateJob($user, $article->id);

        // this usually throws ModelNotFoundException,
        // if the user was not found. Not having anything returned is a good sign
        $this->assertNull($job->handle());
    }

    /**
     * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException
     */
    public function test_fails_authorisation_for_different_user()
    {
        $article = factory(Article::class)->create();

        $anotherUser = factory(User::class)->create();

        $job = new AuthoriseArticleUpdateJob($anotherUser, $article->id);

                // must throw ModelNotFoundException        
        $job->handle();
    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

### Step 3 - Save Article

`// 3. @TODO: update resource`

Generate Job `lucid make:job SaveArticle Article`

Saving an article is simple, we only need its identifier and the new attributes.

{% tabs %}
{% tab title="Code" %}
{% code title="app/Domains/Article/Jobs/SaveArticleJob.php" %}
```php
namespace App\Domains\Article\Jobs;

use Lucid\Foundation\Job;
use App\Data\Models\Article;

class SaveArticleJob extends Job
{
    /**
     * @var string
     */
    private $title;

    /**
     * @var string
     */
    private $content;
    /**
     * @var int
     */
    private $articleId;

    /**
     * Create a new job instance.
     *
     * @param int $articleId
     * @param string $title
     * @param string $content
     */
    public function __construct(int $articleId, string $title, string $content)
    {
        $this->title = $title;
        $this->content = $content;
        $this->articleId = $articleId;
    }

    /**
     * Execute the job.
     *
     * @return int The number of records that were changed.
     *              It will be 1 if update was successful.
     *              Otherwise it's a 0.
     */
    public function handle() : int
    {
        return Article::where('id', $this->articleId)->update([
            'title' => $this->title,
            'content' => $this->content,
        ]);
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Domains/Article/Jobs/SaveArticleJobTest.php" %}
```php
namespace App\Domains\Article\Tests\Jobs;

use App\Data\Models\Article;
use App\Domains\Article\Jobs\SaveArticleJob;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class SaveArticleJobTest extends TestCase
{
    use RefreshDatabase;
    use DatabaseMigrations;

    public function test_save_article_job()
    {
        $title = 'New Unique Title';
        $content = 'Replaced content with this new piece.';

        $article = factory(Article::class)->create();

        $job = new SaveArticleJob($article->id, $title, $content);

        // we expect one record to be updateed only.
        $this->assertEquals(1, $job->handle());
    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

### Step 4 - Add Audit Entry Asynchronously

`// 4. @TODO: save audit activity (async)`

This is a fancy step that in most cases won't be needed, but it's been added to the workflow to showcase working with async using Laravel queues. Logging activity is simply adding a record to an `activity` database table, but adding activity should not be a blocking action for it is not required by the response to the user, to whom we should be responding as quick as possible.

Create `activity` table `php artisan make:migration create_activity_table`

```php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateActivityTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('activity', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('description');
            $table->unsignedBigInteger('user_id');
            $table->timestamps();

            $table->foreign('user_id')
                ->references('id')->on('users')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('activity');
    }
}
```

Create model `app/Data/Models/ActivityLog.php`

```php
namespace App\Data\Models;

use Illuminate\Database\Eloquent\Model;

class ActivityLog extends Model
{
    protected $table = 'activity';

    protected $fillable = ['user_id', 'description'];

    public function actor()
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}
```

Generate job `lucid make:job LogActivity Activity --queue`

You will notice that the generated job `extends QueueableJob` instead of `Job` which will instruct the dispatcher when calling `$this->run(Job)` in the Feature or Operation to run it asynchronously in Laravel [queue](https://laravel.com/docs/master/queues) as configured in the application.

{% tabs %}
{% tab title="Code" %}
{% code title="app/Domains/Activity/Jobs/LogActivityJob.php" %}
```php
namespace App\Domains\Activity\Jobs;

use App\Data\Models\ActivityLog;
use Lucid\Foundation\QueueableJob;

class LogActivityJob extends QueueableJob
{
    /**
     * @var int
     */
    private $userId;

    /**
     * @var string
     */
    private $description;

    /**
     * Create a new job instance.
     *
     * @param int $userId
     * @param string $description
     */
    public function __construct(int $userId, string $description)
    {
        $this->userId = $userId;
        $this->description = $description;
    }

    /**
     * Execute the job.
     *
     * @return ActivityLog
     */
    public function handle() : ActivityLog
    {
        return ActivityLog::create([
            'user_id' => $this->userId,
            'description' => $this->description,
        ]);
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Domains/Activity/Jobs/LogActivityJobTest.php" %}
```php
namespace App\Domains\Activity\Tests\Jobs;

use App\Data\Models\ActivityLog;
use App\Data\Models\User;
use App\Domains\Activity\Jobs\LogActivityJob;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class LogActivityJobTest extends TestCase
{
    use RefreshDatabase;
    use DatabaseMigrations;

    public function test_log_activity_job()
    {
        $user = factory(User::class)->create();

        $job = new LogActivityJob($user->id, "tested logging activity");

        $activity = $job->handle();
        $this->assertInstanceOf(ActivityLog::class, $activity);
        $this->assertEquals($activity->user_id, $user->id);
        $this->assertEquals($activity->description, "tested logging activity");

    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

### Step 5 - Notify Followers

`// 5. @TODO: notify followers (async)`

Notifying followers here is meant as if every follower of this author is subscribed to receive some sort of a notification every time this author publishes a new post. It is made of two steps:

1. Fetching a user's followers
2. Sending them notifications

Similar to our activity log, since it is not essential for the response to the user, we better do these steps asynchronously.

Since each of these steps should be in its own Job according to the Lucid principles, and so that we don't run two jobs every time we need to notify followers, Operations are handy in this scenario. But we will follow an outward approach where we start by creating the jobs and ensuring that they work as expected, then create the operation that runs them, just as we are doing with our Feature here. This approach is recommended because after finishing with the jobs we will know what to the Operation should expect as input parameters based on what the jobs require.

Create `follows` table migration `php artisan:make migration create_followers_table`

which will be our pivot table to link users with their followers.

```php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateFollowsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('follows', function (Blueprint $table) {
            $table->unsignedBigInteger('author_id');
            $table->unsignedBigInteger('follower_id');
            $table->timestamps();

            $table->foreign('author_id')
                ->references('id')->on('users')
                ->onDelete('cascade');

            $table->foreign('follower_id')
                ->references('id')->on('users')
                ->onDelete('cascade');

        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('follows');
    }
}
```

Add relationship method and `Notifiable` trait to `User` model

```php
class User extends Authenticatable
{
    use Notifiable;

    //...

        public function followers()
    {
        return $this->belongsToMany(self::class,
            'follows',
            'author_id',
            'follower_id')
            ->withTimestamps();
    }
}
```

Generate Job to fetch user followers `lucid make:job GetUserFollowers User`

> ### In a real-life scenario you should paginate followers instead of fetching them all at once, this is done this way to keep this demo simple.

{% tabs %}
{% tab title="Code" %}
{% code title="app/Domains/User/Jobs/GetUserFollowersJob.php" %}
```php
namespace App\Domains\User\Jobs;

use Lucid\Foundation\Job;
use App\Data\Models\User;
use Illuminate\Database\Eloquent\Collection;

class GetUserFollowersJob extends Job
{
    /**
     * @var int
     */
    private $userId;

    /**
     * Create a new job instance.
     *
     * @param int $userId
     */
    public function __construct(int $userId)
    {
        $this->userId = $userId;
    }

    /**
     * Execute the job.
     *
     * @return Collection of User models
     */
    public function handle() : Collection
    {
        return User::find($this->userId)->followers;
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Domains/User/Jobs/GetUserFollowersJobTest.php" %}
```php
namespace App\Domains\User\Tests\Jobs;

use App\Data\Models\User;
use App\Domains\User\Jobs\GetUserFollowersJob;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class GetUserFollowersJobTest extends TestCase
{
    use RefreshDatabase;
    use DatabaseMigrations;

    public function test_get_user_followers_job()
    {
        // prepare
        $user = factory(User::class)->create();

        $followers = factory(User::class, 5)->create();

        $user->followers()->saveMany($followers);

        // test
        $job = new GetUserFollowersJob($user->id);

        $retreived = $job->handle();

        $this->assertEquals($followers->toArray(),
            $retreived->makeHidden(['pivot', 'email_verified_at'])->toArray());
    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

Generate notification to be sent `php artisan make:notification ArticlePublished`

We will use this class as-is, no modifications are required here.

Generate Job to send notifications to users `lucid make:job NotifyFollowers User`

{% tabs %}
{% tab title="Code" %}
{% code title="app/Domains/User/Jobs/NotifyFollowersJob.php" %}
```php
namespace App\Domains\User\Jobs;

use Lucid\Foundation\Job;
use App\Notifications\ArticlePublished;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Notification;

class NotifyFollowersJob extends Job
{
    /**
     * @var Collection
     */
    private $followers;

    /**
     * Create a new job instance.
     *
     * @param Collection $followers of User models
     */
    public function __construct(Collection $followers)
    {
        $this->followers = $followers;
    }

    /**
     * Execute the job.
     */
    public function handle()
    {
        Notification::send($this->followers, new ArticlePublished());
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Domains/User/Jobs/NotifyFollowersJobTest.php" %}
```php
namespace App\Domains\User\Tests\Jobs;

use App\Data\Models\User;
use App\Notifications\ArticlePublished;
use Illuminate\Support\Facades\Notification;
use App\Domains\User\Jobs\NotifyFollowersJob;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class NotifyFollowersJobTest extends TestCase
{
    use RefreshDatabase;
    use DatabaseMigrations;

    public function test_notify_followers_job()
    {
        $users = factory(User::class, 5)->create();

        $job = new NotifyFollowersJob($users);

        Notification::shouldReceive('send')
            ->once()->with($users, ArticlePublished::class);

        $job->handle();
    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

Generate Operation to run both jobs `lucid make:operation NotifyFollowers --queue`

And its test. The objective of testing operations is **to ensure they run the correct jobs with the correct parameters in the correct sequence**. It is not necessary to run the actual job because we would be testing the job twice while we should be keen not to reinvent the wheel nor replicating tests, we rely on the Job's tests to provide its guarantee.

{% tabs %}
{% tab title="Code" %}
{% code title="app/Operations/NotifyFollowersOperation.php" %}
```php
namespace App\Operations;

use Lucid\Foundation\QueueableOperation;
use Illuminate\Http\Request;
use App\Domains\User\Jobs\NotifyFollowersJob;
use App\Domains\User\Jobs\GetUserFollowersJob;

class NotifyFollowersOperation extends QueueableOperation
{
    /**
     * @var int
     */
    private $userId;

    /**
     * NotifyFollowersOperation constructor.
     * @param int $userId
     */
    public function __construct(int $userId)
    {
        $this->userId = $userId;
    }

    public function handle()
    {
        $followers = $this->run(GetUserFollowersJob::class, [
            'userId' => $this->userId
        ]);

        $this->run(NotifyFollowersJob::class, compact('followers'));
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Operations/NotifyFollowersOperationTest.php" %}
```php
namespace App\Tests\Operations;

use Mockery;
use Tests\TestCase;
use App\Data\Models\User;
use App\Operations\NotifyFollowersOperation;
use App\Domains\User\Jobs\NotifyFollowersJob;
use App\Domains\User\Jobs\GetUserFollowersJob;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class NotifyFollowersOperationTest extends TestCase
{
    use RefreshDatabase;
    use DatabaseMigrations;

    public function tearDown(): void
    {
        parent::tearDown();

        Mockery::close();
    }

    public function test_notifying_followers()
    {
        $user = factory(User::class)->create();
        $followers = factory(User::class, 5)->create();

        $op = Mockery::mock(NotifyFollowersOperation::class, [$user->id])
            ->makePartial();

        $op->shouldReceive('run')
            ->once()->with(GetUserFollowersJob::class, ['userId' => $user->id])
            ->andReturn($followers);

        $op->shouldReceive('run')
            ->once()->with(NotifyFollowersJob::class, compact('followers'));

        $op->handle();
    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

### Step 6 - Respond with Update Status

Simply return a JSON response using built-in `Http\RespondWithJsonJob` as seen in the next step.

### Step 7 -  Finally: Compose Feature

Now that we have all the jobs ready and we're sure that they are working as expected through their unit tests, all we have to do now is run them in sequence in our Feature class and pass the correct parameters and everything should work as expected.

Create controller `lucid make:controller Article --plain`&#x20;

Add `update` controller method to serve the feature, and route in `routes/web.php`

`Route::put('/articles/{articleId}', 'ArticleController@update');`

{% code title="app/Http/Controllers/ArticleController.php" %}
```php
namespace App\Http\Controllers;

use Lucid\Foundation\Http\Controller;
use App\Features\UpdateArticleFeature;

class ArticleController extends Controller
{
    public function update($articleId)
    {
        return $this->serve(UpdateArticleFeature::class, 
            ['articleId' => intval($articleId)]);
    }
}

```
{% endcode %}

Fill the feature's `handle` method and test it.

{% tabs %}
{% tab title="Code" %}
{% code title="app/Features/UpdateArticleFeature.php" %}
```php
namespace App\Features;

use Auth;
use Lucid\Foundation\Feature;
use Illuminate\Http\Request;
use App\Domains\Article\Jobs\SaveArticleJob;
use App\Operations\NotifyFollowersOperation;
use App\Domains\Activity\Jobs\LogActivityJob;
use App\Domains\Http\Jobs\RespondWithJsonJob;
use App\Domains\Article\Jobs\ValidateArticleInputJob;
use App\Domains\Article\Jobs\AuthoriseArticleUpdateJob;

class UpdateArticleFeature extends Feature
{
    /**
     * @var int
     */
    private $articleId;

    /**
     * CreateArticleFeature constructor.
     *
     * @param int $articleId
     */
    public function __construct(int $articleId)
    {
        $this->articleId = $articleId;
    }

    /**
     * @param Request $request
     */
    public function handle(Request $request)
    {
        $this->run(new ValidateArticleInputJob($request->input()));

        $this->run(AuthoriseArticleUpdateJob::class, [
            'user' => Auth::user(),
            'articleId' => $this->articleId,
        ]);

        $isSuccess = $this->run(SaveArticleJob::class, [
            'articleId' => $this->articleId,
            'title'=> $request->input('title'),
            'content' => $request->input('content'),
        ]);

        $this->run(LogActivityJob::class, [
            'userId' => Auth::user()->id,
            'description' => "Article updated ".$this->articleId,
        ]);

        $this->run(NotifyFollowersOperation::class, [
            'userId' => Auth::user()->id,
        ]);

        return $this->run(new RespondWithJsonJob(['success' => $isSuccess]));
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Test" %}
{% code title="tests/Features/UpdateArticleFeatureTest.php" %}
```php
namespace App\Tests\Features;

use Tests\TestCase;
use App\Data\Models\User;
use App\Data\Models\Article;
use App\Features\UpdateArticleFeature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class UpdateArticleFeatureTest extends TestCase
{
    use RefreshDatabase;
    use DatabaseMigrations;

    public function test_updating_article_feature()
    {
        $article = factory(Article::class)->create();
        $user = $article->user;

        $this->actingAs($user)->put("/articles/$article->id", [
            'title' => 'My Updated New Title',
            'content' => 'Shiny shiny, shiny new content.',
        ])->assertJson([
            'status'=> 200,
            'data' => [
                'success' => true,
            ]
        ]);

        $retrieved = Article::find($article->id);

        $this->assertEquals('My Updated New Title', $retrieved->title);
        $this->assertEquals('Shiny shiny, shiny new content.', $retrieved->content);
    }

    /**
     * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException
     */
    public function test_updating_article_fails_when_unauthorised()
    {
        $article = factory(Article::class)->create();
        $anotherUser = factory(User::class)->create();

        $this->actingAs($anotherUser)->put("/articles/$article->id", [
            'title' => 'My Updated New Title',
            'content' => 'Shiny shiny, shiny new content.',
        ])->assertJson([
            'status'=> 200,
            'data' => [
                'success' => true,
            ]
        ]);
    }
}

```
{% endcode %}
{% endtab %}
{% endtabs %}

## Notable Observation

Through this workflow, it is important to note how tests cover the scope of the component only and keeps us from breaking the boundaries where we shouldn't. Especially when it comes to the feature test where it is kept to what it is responsible for, without having to repeat the tests that's been covered by the jobts themselves. However, it might be important to test edge cases at the feature level to ensure that the responses from our backend are consistent and as expected.

## FAQ

**Why sometimes we use UserId and other times the whole User?**

Because of the scope. For example: `AuthoriseArticleUpdateJob` might require more information about the user in the future than their ID because the nature of what it does - authorisation. While other jobs such as `LogActivityJob` and `NotifyFollowersOperation` are only concerned about the user's identifier, regardless of where and how this was retrieved.

Doing this allows for the reuse of these jobs beyond the scope of this feature alone. We can use them anywhere else (for example from a command where users aren't authenticated) without having to workaround authentication where it doesn't belong.

**Could we have improved logging activity?**

Yes. For example, create a job for each activity (i.e. `LogArticleUpdateActivityJob` instead of a generic `LogActivityJob`. It would be preferred over having a string in the feature which is recommended to be avoided when possible but kept here for the simplicity of this exercise.

**Can we add more safety to this code?**

Yes, plenty! A simple addition would be to ensure there's a session before proceeding with the feature to avoid errors such as:

```php
Symfony\Component\Debug\Exception\FatalThrowableError: Argument 1 passed to 
App\Domains\Article\Jobs\AuthoriseArticleUpdateJob::__construct() 
must be an instance of App\Data\Models\User, null given
```

This could be done around the HTTP Kernel like in the middleware area.

Another safety check would be to wrap async calls with a `try/catch` block so that failures in there don't affect the user's response (namely `LogActivityJob` and `NotifyFollowersOperation` because they do not dictate the actual status of the update.
