Skip to main content

4 posts tagged with "workflow"

View All Tags

· 4 min read
Richard

captionless image

Have you ever spent hours tracking down a frontend bug that only happens in production? When working with web applications, debugging frontend issues can be challenging. Console errors and unexpected UI behaviors often require careful inspection and reproducible test cases. Wouldn’t it be great if you could automate this process, capture errors, and even record a video of the session for later analysis?

With Playwright and Laravel Workflow, you can achieve just that! In this post, I’ll walk you through an automated workflow that:

  • Loads a webpage and captures console errors.
  • Records a video of the session.
  • Converts the video to an MP4 format for easy sharing.
  • Runs seamlessly in a GitHub Codespace.

The Stack

  • Playwright: A powerful browser automation tool for testing web applications.
  • Laravel Workflow: A durable workflow engine for handling long-running, distributed processes.
  • FFmpeg: Used to convert Playwright’s WebM recordings to MP4 format.

captionless image

1. Capturing Errors and Video with Playwright

The Playwright script automates a browser session, navigates to a given URL, and logs any console errors. It also records a video of the entire session.

import { chromium } from 'playwright';
import path from 'path';
import fs from 'fs';

(async () => {
const url = process.argv[2];
const videoDir = path.resolve('./videos');

if (!fs.existsSync(videoDir)) {
fs.mkdirSync(videoDir, { recursive: true });
}

const browser = await chromium.launch({ args: ['--no-sandbox'] });
const context = await browser.newContext({
recordVideo: { dir: videoDir }
});

const page = await context.newPage();

let errors = [];

page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});

try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 10000 });
} catch (error) {
errors.push(`Page load error: ${error.message}`);
}
const video = await page.video().path();

await browser.close();

console.log(JSON.stringify({ errors, video }));
})();

2. Running the Workflow

A Laravel console command (php artisan app:playwright) starts the workflow which:

  • Runs the Playwright script and collects errors.
  • Converts the video from .webm to .mp4 using FFmpeg.
  • Returns the errors and the final video file path.
namespace App\Console\Commands;

use App\Workflows\Playwright\CheckConsoleErrorsWorkflow;
use Illuminate\Console\Command;
use Workflow\WorkflowStub;

class Playwright extends Command
{
protected $signature = 'app:playwright';

protected $description = 'Runs a playwright workflow';

public function handle()
{
$workflow = WorkflowStub::make(CheckConsoleErrorsWorkflow::class);
$workflow->start('https://example.com');
while ($workflow->running());
$this->info($workflow->output()['mp4']);
}
}

3. The Workflow

namespace App\Workflows\Playwright;

use Workflow\ActivityStub;
use Workflow\Workflow;

class CheckConsoleErrorsWorkflow extends Workflow
{
public function execute(string $url)
{
$result = yield ActivityStub::make(CheckConsoleErrorsActivity::class, $url);

$mp4 = yield ActivityStub::make(ConvertVideoActivity::class, $result['video']);

return [
'errors' => $result['errors'],
'mp4' => $mp4,
];
}
}

4. Running Playwright

namespace App\Workflows\Playwright;

use Illuminate\Support\Facades\Process;
use Workflow\Activity;

class CheckConsoleErrorsActivity extends Activity
{
public function execute(string $url)
{
$result = Process::run([
'node', base_path('playwright-script.js'), $url
])->throw();

return json_decode($result->output(), true);
}
}

5. Video Conversion with FFmpeg

The Playwright recording is stored in WebM format, but we need an MP4 for wider compatibility. Laravel Workflow runs this process asynchronously.

namespace App\Workflows\Playwright;

use Illuminate\Support\Facades\Process;
use Workflow\Activity;

class ConvertVideoActivity extends Activity
{
public function execute(string $webm)
{
$mp4 = str_replace('.webm', '.mp4', $webm);

Process::run([
'ffmpeg', '-i', $webm, '-c:v', 'libx264', '-preset', 'fast', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', $mp4
])->throw();

unlink($webm);

return $mp4;
}
}

🚀 Try It Out in a GitHub Codespace

You don’t need to set up anything on your local machine. Everything is already configured in the Laravel Workflow Sample App.

Steps to Run the Playwright Workflow

  • Open the Laravel Workflow Sample App on GitHub: laravel-workflow/sample-app
  • Click “Create codespace on main” to start a pre-configured development environment.

captionless image

  • Once the Codespace is ready, run the following commands in the terminal:
php artisan migrate
php artisan queue:work
  • Then open a second terminal and run this command:
php artisan app:playwright

That’s it! The workflow will execute, capture console errors, record a video, and convert it to MP4. You can find the video in the videos folder. Take a look at the sample app’s README.md for more information on other workflows and how to view the Waterline UI.

Conclusion

By integrating Playwright with Laravel Workflow, we’ve automated frontend error detection and debugging. This setup allows teams to quickly identify and resolve issues, all while leveraging Laravel’s queue system to run tasks asynchronously.

🔗 Next Steps

Happy automating! 🚀

· 2 min read
Richard

captionless image

One of the strengths of the Laravel ecosystem is its flexibility, thanks to a myriad of community-driven packages that enhance the framework’s capabilities. The laravel-workflow and spatie/laravel-tags packages are two such examples, and in this post, we'll integrate them together to make workflows taggable.

Installation Instructions

Before diving into the code, let’s ensure both libraries are properly installed:

  1. Install Laravel Workflow and Spatie Laravel Tags.
composer require laravel-workflow/laravel-workflow spatie/laravel-tags
  1. Both packages include migrations that must be published.
php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="migrations"
php artisan vendor:publish --provider="Spatie\Tags\TagsServiceProvider" --tag="tags-migrations"
  1. Run the migrations.
php artisan migrate

Publishing Configuration

To extend Laravel Workflow, publish its configuration file:

php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="config"

Extending Workflows to Support Tags

We need to extend the StoredWorkflow model of laravel-workflow to support tagging.

namespace App\Models;

use Spatie\Tags\HasTags;
use Workflow\Models\StoredWorkflow as BaseStoredWorkflow;
use Workflow\WorkflowStub;

class StoredWorkflow extends BaseStoredWorkflow
{
use HasTags;

public static function tag(WorkflowStub $workflow, $tag): void
{
$storedWorkflow = static::find($workflow->id());
if ($storedWorkflow) {
$storedWorkflow->attachTag($tag);
}
}

public static function findByTag($tag): ?WorkflowStub
{
$storedWorkflow = static::withAnyTags([$tag])->first();
if ($storedWorkflow) {
return WorkflowStub::fromStoredWorkflow($storedWorkflow);
}
}
}

Modify the Configuration

In config/workflow.php, update this line:

'stored_workflow_model' => Workflow\Models\StoredWorkflow::class,

To:

'stored_workflow_model' => App\Models\StoredWorkflow::class,

This ensures Laravel Workflow uses the extended model.

Running Tagged Workflows

With the taggable StoredWorkflow ready, create a console command to create, tag, retrieve, and run a workflow.

namespace App\Console\Commands;

use App\Models\StoredWorkflow;
use App\Workflows\Simple\SimpleWorkflow;
use Illuminate\Console\Command;
use Workflow\WorkflowStub;

class Workflow extends Command
{
protected $signature = 'workflow';

protected $description = 'Runs a workflow';

public function handle()
{
// Create a workflow and tag it
$workflow = WorkflowStub::make(SimpleWorkflow::class);
StoredWorkflow::tag($workflow, 'tag1');

// Find the workflow by tag and start it
$workflow = StoredWorkflow::findByTag('tag1');
$workflow->start();

while ($workflow->running());

$this->info($workflow->output());
}
}

Conclusion

By integrating laravel-workflow with spatie/laravel-tags, we've enabled tagging for workflows, making management more intuitive in larger applications. Thanks to Laravel’s extensible nature, endless possibilities await developers leveraging these powerful packages.

· 3 min read
Richard

captionless image

Introduction

Before we begin, let’s understand the scenario. We are building an image moderation system where:

  1. Every image undergoes an initial AI check to determine if it’s safe.
  2. If the AI deems the image unsafe, it’s automatically logged and deleted.
  3. If it’s potentially safe, a human moderator is alerted to further review the image. They have the option to approve or reject the image.
  4. Approved images are moved to a public location, whereas rejected images are deleted.

Laravel Workflow

Laravel Workflow is designed to streamline and organize complex processes in applications. It allows developers to define, manage, and execute workflows seamlessly. You can find installation instructions here.

ClarifAI API

ClarifAI provides AI-powered moderation tools for analyzing visual content. They offer a free plan with up to 1,000 actions per month.

1. Store your credentials in .env.

CLARIFAI_API_KEY=key
CLARIFAI_APP=my-application
CLARIFAI_WORKFLOW=my-workflow
CLARIFAI_USER=username

2. Add the service to config/services.php.

'clarifai' => [
'api_key' => env('CLARIFAI_API_KEY'),
'app' => env('CLARIFAI_APP'),
'workflow' => env('CLARIFAI_WORKFLOW'),
'user' => env('CLARIFAI_USER'),
],

3. Create a service at app/Services/ClarifAI.php.

namespace App\Services;

use Illuminate\Support\Facades\Http;

class ClarifAI
{
private $apiKey;
private $apiUrl;

public function __construct()
{
$app = config('services.clarifai.app');
$workflow = config('services.clarifai.workflow');
$user = config('services.clarifai.user');
$this->apiKey = config('services.clarifai.api_key');
$this->apiUrl = "https://api.clarifai.com/v2/users/{$user}/apps/{$app}/workflows/{$workflow}/results/";
}

public function checkImage(string $image): bool
{
$response = Http::withToken($this->apiKey, 'Key')
->post($this->apiUrl, ['inputs' => [
['data' => ['image' => ['base64' => base64_encode($image)]]],
]]);

return collect($response->json('results.0.outputs.0.data.concepts', []))
->filter(fn ($value) => $value['name'] === 'safe')
->map(fn ($value) => round((float) $value['value']) > 0)
->first() ?? false;
}
}

Creating the Workflow

namespace App\Workflows;

use Workflow\ActivityStub;
use Workflow\SignalMethod;
use Workflow\WorkflowStub;
use Workflow\Workflow;

class ImageModerationWorkflow extends Workflow
{
private bool $approved = false;
private bool $rejected = false;

#[SignalMethod]
public function approve()
{
$this->approved = true;
}

#[SignalMethod]
public function reject()
{
$this->rejected = true;
}

public function execute($imagePath)
{
$safe = yield from $this->check($imagePath);

if (! $safe) {
yield from $this->unsafe($imagePath);
return 'unsafe';
}

yield from $this->moderate($imagePath);

return $this->approved ? 'approved' : 'rejected';
}

private function check($imagePath)
{
return yield ActivityStub::make(AutomatedImageCheckActivity::class, $imagePath);
}

private function unsafe($imagePath)
{
yield ActivityStub::all([
ActivityStub::make(LogUnsafeImageActivity::class, $imagePath),
ActivityStub::make(DeleteImageActivity::class, $imagePath),
]);
}

private function moderate($imagePath)
{
while (true) {
yield ActivityStub::make(NotifyImageModeratorActivity::class, $imagePath);

$signaled = yield WorkflowStub::awaitWithTimeout('24 hours', fn () => $this->approved || $this->rejected);

if ($signaled) break;
}
}
}

Activities

Automated Image Check

namespace App\Workflows;

use App\Services\ClarifAI;
use Illuminate\Support\Facades\Storage;
use Workflow\Activity;

class AutomatedImageCheckActivity extends Activity
{
public function execute($imagePath)
{
return app(ClarifAI::class)
->checkImage(Storage::get($imagePath));
}
}

Logging Unsafe Images

namespace App\Workflows;

use Illuminate\Support\Facades\Log;
use Workflow\Activity;

class LogUnsafeImageActivity extends Activity
{
public function execute($imagePath)
{
Log::info('Unsafe image detected at: ' . $imagePath);
}
}

Deleting Images

namespace App\Workflows;

use Illuminate\Support\Facades\Storage;
use Workflow\Activity;

class DeleteImageActivity extends Activity
{
public function execute($imagePath)
{
Storage::delete($imagePath);
}
}

Starting and Signaling the Workflow

$workflow = WorkflowStub::make(ImageModerationWorkflow::class);
$workflow->start('tmp/good.jpg');

For approvals or rejections:

$workflow = WorkflowStub::load($id);
$workflow->approve();
// or
$workflow->reject();

Conclusion

Laravel Workflow provides a structured approach to handle complex processes like image moderation. It supports asynchronous processing, external API integrations, and modular design for scalability. Thanks for reading!

· 4 min read
Richard

captionless image

In the evolving landscape of microservices, communication has always been a focal point. Microservices can interact in various ways, be it through HTTP/REST calls, using messaging protocols like RabbitMQ or Kafka, or even employing more recent technologies like gRPC. Yet, regardless of the communication method, the goal remains the same: seamless, efficient, and robust interactions. Today, we’ll explore how Laravel Workflow can fit into this picture and optimize the communication between microservices in a unique way.

The Challenge

In a microservices architecture, decoupling is the name of the game. You want each service to have a single responsibility, to be maintainable, and to be independently deployable. Yet, in the world of workflows, this becomes challenging. How do you split a workflow from its activity and yet ensure they communicate seamlessly?

Laravel Workflow to the Rescue!

Laravel Workflow handles the discovery and orchestration for you! With a shared database and queue connection, you can have your workflow in one Laravel app and its activity logic in another.

Defining Workflows and Activities

1. Create a workflow.

use Workflow\ActivityStub;
use Workflow\Workflow;

class MyWorkflow extends Workflow
{
public function execute($name)
{
$result = yield ActivityStub::make(MyActivity::class, $name);
return $result;
}
}

2. Create an activity.

use Workflow\Activity;

class MyActivity extends Activity
{
public function execute($name)
{
return "Hello, {$name}!";
}
}

3. Run the workflow.

use Workflow\WorkflowStub;

$workflow = WorkflowStub::make(MyWorkflow::class);
$workflow->start('world');
while ($workflow->running());
$workflow->output();
// Output: 'Hello, world!'

The workflow will manage the activity and handle any failures, retries, etc. Think of workflows like job chaining on steroids because you can have conditional logic, loops, return a result that can be used in the next activity, and write everything in typical PHP code that is failure tolerant.

Balancing Shared and Dedicated Resources

When working with microservices, it’s common for each service to have its dedicated resources, such as databases, caches, and queues. However, to facilitate communication between workflows and activities across services, a shared connection (like a database or queue) becomes essential. This shared connection acts as a bridge for data and task exchanges while ensuring:

  1. Isolation: Dedicated resources prevent cascading failures.
  2. Performance: Each service can be optimized independently.
  3. Security: Isolation limits potential attack vectors.

Step-By-Step Integration

1. Install laravel-workflow in all microservices.

Follow the installation guide.

2. Create a shared database/redis connection in all microservices.

// config/database.php
'connections' => [
'shared' => [
'driver' => 'mysql',
'host' => env('SHARED_DB_HOST', '127.0.0.1'),
'database' => env('SHARED_DB_DATABASE', 'forge'),
'username' => env('SHARED_DB_USERNAME', 'forge'),
'password' => env('SHARED_DB_PASSWORD', ''),
],
],

3. Configure a shared queue connection.

// config/queue.php
'connections' => [
'shared' => [
'driver' => 'redis',
'connection' => 'shared',
'queue' => env('SHARED_REDIS_QUEUE', 'default'),
],
],

4. Ensure only one microservice publishes Laravel Workflow migrations.

Update the migration to use the shared database connection.

// database/migrations/..._create_workflows_table.php
class CreateWorkflowsTable extends Migration
{
protected $connection = 'shared';
}

5. Extend workflow models in each microservice to use the shared connection.

// app/Models/StoredWorkflow.php
namespace App\Models;

use Workflow\Models\StoredWorkflow as BaseStoredWorkflow;

class StoredWorkflow extends BaseStoredWorkflow
{
protected $connection = 'shared';
}

6. Publish Laravel Workflow config and update it with shared models.

php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="config"

7. Set workflows and activities to use the shared queue.

// app/Workflows/MyWorkflow.php
class MyWorkflow extends Workflow
{
public $connection = 'shared';
public $queue = 'workflow';
}
// app/Workflows/MyActivity.php
class MyActivity extends Activity
{
public $connection = 'shared';
public $queue = 'activity';
}

8. Ensure microservices define empty counterparts for workflow and activity classes.

In the workflow microservice:

class MyWorkflow extends Workflow
{
public $connection = 'shared';
public $queue = 'workflow';

public function execute($name)
{
yield ActivityStub::make(MyActivity::class, $name);
}
}
class MyActivity extends Activity
{
public $connection = 'shared';
public $queue = 'activity';
}

In the activity microservice:

class MyWorkflow extends Workflow
{
public $connection = 'shared';
public $queue = 'workflow';
}
class MyActivity extends Activity
{
public $connection = 'shared';
public $queue = 'activity';

public function execute($name)
{
return "Hello, {$name}!";
}
}

9. Ensure all microservices have the same APP_KEY in their .env file.

This is crucial for proper job serialization across services.

10. Run queue workers in each microservice.

php artisan queue:work shared --queue=workflow
php artisan queue:work shared --queue=activity

Conclusion

By following the steps above, you can ensure seamless interactions between microservices while maintaining modularity and scalability. Laravel Workflow takes care of the discovery and orchestration for you. 🚀

Thanks for reading!