Darkwood · PHP 8.5 · Laravel · Flow

Laravel Pipeline
& Darkwood Flow

Two execution models for
composing PHP steps

Symfony Console demo · demo:pipeline:compare

Composition
≠
Execution
@matyo91

The overloaded word

"Pipeline" means composition.
Not execution.

HTTP middleware
Unix pipes
CI/CD stages
Dataflow / ETL

Same word. Different execution contracts.

01 | pipeline @matyo91

Core idea

A pipeline defines
how work is composed.

The runtime defines
how that work executes.

Ask: "what are the steps?" and "how does it execute?"

02 | thesis @matyo91

Laravel Pipeline

The closure onion

array_reverse

Pipes folded outer-first

array_reduce

Nested closures

One passable

One sync call stack

$pipeline = array_reduce(
    array_reverse($pipes),
    $carry,
    $destination
);
return $pipeline($passable);
03 | laravel-onion @matyo91

Laravel execution

A(B(C(destination)))

flowchart TB
  subgraph onion ["through A, B, C"]
    A["Pipe A\noutermost"]
    B["Pipe B"]
    C["Pipe C"]
    D["destination"]
  end
  A --> B --> C --> D

$next = the rest of the chain, already folded.

04 | laravel-diagram @matyo91

Why $next matters

The middleware contract

Before

Run logic, then $next($passable)

After

$response = $next($passable), then transform

Short-circuit

Return without calling $next → chain stops

Perfect for one HTTP request · one passable · one call stack

05 | next @matyo91

The demo

Same pipeline. Two runtimes.

Fetch
→
Hash
→
Save
0.8s
0.3s
0.6s
0.2s
0.5s
php bin/console demo:pipeline:compare

In-memory fixture · controlled timers · not a benchmark

06 | demo @matyo91

Common misconception

Fibers do not make blocking code async.

Still blocks the thread

  • sleep() / usleep()
  • Blocking HTTP clients
  • file_get_contents()

Useful overlap needs

  • Event-loop timers
  • Non-blocking I/O integration
  • Driver that schedules waits

Demo uses AmpDriver + Amp\delay() — not raw Fibers alone.

07 | fibers @matyo91

Darkwood Flow

Nodes · packets · driver

Flow

Transformation node

Job

T1 → T2

Ip

Instruction packet

Driver

Runtime scheduler

await()

Completion barrier · returns void

08 | flow-model @matyo91

Flow execution

Five packets · one pipeline graph

flowchart LR
  subgraph input ["Submit"]
    I1[Ip task 1]
    I2[Ip task 2]
    I5[Ip task 5]
  end
  F[FetchJob]
  H[HashJob]
  S[SaveJob]
  C[Collector]
  I1 --> F
  I2 --> F
  I5 --> F
  F --> H --> S --> C
foreach ($tasks as $task) { $flow(new Ip($task)); }
$flow->await();
09 | flow-diagram @matyo91

AmpDriver

Event-loop scheduling · one thread

sequenceDiagram
    participant T1 as Task1
    participant Loop as RevoltEventLoop
    participant T2 as Task2
    T1->>Loop: Amp delay 0.8s
    T2->>Loop: Amp delay 0.3s
    Note over T1,T2: timers registered
    Loop->>T2: resume 0.3s
    Loop->>T1: resume 0.8s
✓ concurrent waits
✗ parallel CPU
10 | amp @matyo91

Measurements

Scheduling behavior — not a benchmark

Sync onion

~2.42 s

delays accumulate (Σ 2.4s)

Flow + Amp

~0.81 s

delays overlap (max 0.8s)

gantt
    title Fetch phase only
    dateFormat X
    axisFormat %Ls
    section Sync
    T1 :0, 800
    T2 :800, 1100
    T3 :1100, 1700
    T4 :1700, 1900
    T5 :1900, 2400
    section Flow
    T1f :0, 800
    T2f :0, 300
    T3f :0, 600
    T4f :0, 200
    T5f :0, 500
11 | timing @matyo91

What the demo proves

Evidence, not hype

  • Independent timer waits can overlap on one thread
  • Multiple Ip packets traverse the same stages
  • Logs interleave — task #2 fetch starts before task #1 fetch ends
  • Identical hashes and output files (PHPUnit verified)
[FLOW][task #1][fetch] start
[FLOW][task #2][fetch] start
[FLOW][task #5][fetch] start
[FLOW][task #4][fetch] end
12 | proves @matyo91

What it does NOT prove

Stay precise

Not universal speed

Flow is not always faster

Not slow Laravel

Pipeline is not poorly designed

Not parallel CPU

Single OS thread throughout

Not async HTTP

Blocking I/O would still block

13 | limits @matyo91

Comparison

Different problems · different tools

Laravel Pipeline

  • Simple onion (~30 LOC core)
  • Sync · one passable
  • Explicit $next
  • Returns from then()
  • Middleware / commands

Darkwood Flow

  • Nodes · drivers · dispatchers
  • Async-capable · many Ips
  • await() returns void
  • Terminal collector required
  • Overlapping wait-heavy work

Sync demo: 5 sequential onions · Flow demo: 5 concurrent packets

14 | compare @matyo91

What Flow can learn

Laravel got ergonomics right

await()Returns void — easy to forget
resultsTerminal collector, not thenReturn()
IpManual wrapping per input
bundleSymfony services config is empty
ideassink() · runAll() · TraceSubscriber · hrtime() delays
15 | api-lessons @matyo91

Choosing the model

Match runtime to workload

Laravel Pipeline

  • One passable per call
  • Middleware / command intercept
  • Short-circuit via $next
  • Sync, fast steps

Darkwood Flow

  • Many independent packets
  • Non-blocking driver waits
  • Per-node error handling
  • Runtime flexibility

They coexist. Different layers, different models.

16 | choose @matyo91

Takeaway

Both are pipelines.
They are not the same machine.

Always ask:

How does it execute?

Onion
or
Dataflow
—
composition ≠ execution
@matyo91

Resources

Go deeper

  • darkwood/content/laravel-pipeline — runnable demo
  • demo:pipeline:sync · demo:pipeline:flow · demo:pipeline:compare
  • darkwood/flow — dataflow component
  • Blog article — execution models deep dive

Re-run timings on your machine before citing exact numbers in slides.

17 | resources @matyo91