Log in
Seblog.nl

Seblog.nl

Ik, terwijl ik mijn veel te warme laptop na een nacht van de lader haal: ‘Wat heb jij zitten doen? Jij hebt naar mijn foto’s zitten kijken hè?’ Laptop zegt niets terug.

[laravel/ideas] Calling a Pipeline multiple times

So, currently the Illuminate\Pipeline\Pipeline is used for Route Middleware and Job Middleware. In both cases, the Pipeline is used once: you new up an Instance and pass it the Request or Job, then use the result. It is clearly optimized for that use case.

I have a use-case in which I would like to call a single Pipeline (consisting of the same pipes) over and over again. When I used the Pipeline as is, I was surprised to learn that each time I run the pipeline with ->then(), I would get new instances of my Middleware. An example:

$pipeline = resolve(Pipeline::class)
    ->through($pipes);

$handle = fopen('logs.txt', 'r');
while ($line = fgets($handle)) {
    $result = $pipeline
        ->send($line)
        ->then(fn($line) => $line);
    // ...
}

This will create new instances of all $pipes for each line in the file. This is undesirable if you want to do something with the lines based on a state of the pipe. As a silly example:

class LogRowCounter
{
    protected $counter = 0;

    public function handle($line, $next)
    {
        $this->counter++;

        return $next($this->counter . ' - ' . $line);
    }
}

Apart from resolving all the $pipes, a lot of checks are also done while running the pipeline, meaning it happens in the while loop of my example. Some of those things could be done beforehand.

Reusable Pipeline

To make the Pipeline reusable, I would propose a new method called prepare(). (I was also thinking about compile(), but ‘prepare’ is used in some protected methods of Pipeline already.) It would extract running the array_reduce() from then() and return the resulting closure, and then() can call it.

    public function prepare(Closure $destination)
    {
        $pipeline = array_reduce(
            array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
        );

        return $pipeline;
    }

    public function then(Closure $destination)
    {
        $pipeline = $this->prepare($destination);

        return $pipeline($this->passable);
    }

This would still resolve all the pipes on ‘runtime’ though, because of how the closures are structured. Here is a new version of carry(), which resolves the pipes outside of the returned closure. The outer closure returned is meant for array_reduce(), like the current version; didn't want to refactor that.

    protected function carry()
    {
        return function ($stack, $pipe) {
            if (is_string($pipe)) {
                [$name, $parameters] = $this->parsePipeString($pipe);

                $pipe = $this->getContainer()->make($name);
            } else {
                $parameters = [];
            }

            return function ($passable) use ($stack, $pipe, $parameters) {
                try {
                    if (is_callable($pipe)) {
                        return $pipe($passable, $stack);
                    }

                    $parameters = array_merge([$passable, $stack], $parameters);

                    $carry = method_exists($pipe, $this->method)
                                    ? $pipe->{$this->method}(...$parameters)
                                    : $pipe(...$parameters);

                    return $this->handleCarry($carry);
                } catch (Exception $e) {
                    return $this->handleException($passable, $e);
                } catch (Throwable $e) {
                    return $this->handleException($passable, new FatalThrowableError($e));
                }
            };
        };
    }

All Pipeline tests pass, and the following new test would pass too:

    public function testPipelinePrepared()
    {
        $pipeline = (new Pipeline(new Container))
                    ->through([
                        PipelineTestPipeOne::class,
                        PipelineTestStatefulPipe::class
                    ])
                    ->prepare(function ($piped) {
                        return $piped;
                    });

        $result = $pipeline('foo');

        $this->assertSame('foo', $result);
        $this->assertSame('foo', $_SERVER['__test.pipe.one']);
        $this->assertSame([], $_SERVER['__test.pipe.state']);

        $result = $pipeline('bar');

        $this->assertSame('bar', $result);
        $this->assertSame('bar', $_SERVER['__test.pipe.one']);
        $this->assertSame([0, 1], $_SERVER['__test.pipe.state']);

        unset($_SERVER['__test.pipe.one']);
        unset($_SERVER['__test.pipe.state']);
    }
}

class PipelineTestStatefulPipe
{
    protected $count = 0;

    public function handle($piped, $next)
    {
        $_SERVER['__test.pipe.state'][] = $this->count++;

        return $next($piped);
    }
}

Things to note

First off: I’ve taken the liberty to rename the ! is_object($pipe) check to is_string($pipe), because the very next thing we do is call $this->parsePipeString($pipe) with it.

Because of the way the closure is bound, the following code would work, and it seems like I can put it to use, but it looks wonky and weird too:

$filename = 'logs.txt';
$pipeline = resolve(Pipeline::class)
    ->through($pipes);

$run = $pipeline->prepare(fn($line) => $line);

// Call all `init($filename, $next)` methods
$pipeline->via('init');
$run($filename);

// Set method back to 'handle'
$pipeline->via('handle');

$handle = fopen($filename, 'r');
while ($line = fgets($handle)) {
    // Call all `handle($line, $next)` methods
    $result = $run($line);

    // ...
}

… that syntax makes me feel like Pipeline should store the Closure as a property somewhere, so prepare() returns $this, and then add an __invoke() or run() to Pipeline, so you can at least use the same variable instead of $pipeline and $run.

Alternative solutions

While we could do the above, I also have a different solution: singletons in the Container.

I originally rejected this (hence the work above) because I don’t want my stateful pipes to be global singletons. I want the pipes to belong to the Pipeline: if the Pipeline goes, so go the references to the pipes.

Think for example of the above file-handling example, but as a queued Job: I don’t want the state of the pipes from one run to be around when I process the next log-file.

But: this last point can also be resolved by creating a new Container instance per Job, and giving that to the Pipeline while constructing it. You can bind singletons into that separate container, which will belong to the pipeline.

Note that in that case – if we don’t change the Pipeline class – you’re still making the calls to the Container on ‘runtime’, which is not optimal. On the other hand, it might not be a very big deal.

I don't really like the way I have to new-up or resolve the Pipeline in this case, but it is doable.


I’d be happy to work this into a PR, but I thought this might be a better start, especially since I’m still debating whether or not I got a point here.

Thanks for reading, I’m curious about your thoughts.

‪Ik heb één klok in mijn huis die niet zelf naar wintertijd gaat. Het was leuk om te zien hoe laat het ‘eigenlijk’ was, vond ik, voor een paar weken. Nu heb ik ‘m op lokale zonnetijd gezet, dat voelt echter.‬

Really liking the arrow functions in PHP 7.4 so far! Unfortunately already found one missing feature: you can't throw Exceptions from them. The following results in a 'unexpected T_THROW' syntax error:

fn() => throw new Exception('nope');

Would've cleaned up my test, but alas.

De oorkussens van mijn Bose QuietComfort 35 II waren na twee jaar dagelijks gebruik echt helemaal versleten en aan één kant zelfs opengesprongen. Jammer, want het is echt een geweldige koptelefoon. Maar gelukkig kon ik nieuwe oorkussens bestellen, en die zijn net binnen.

Nieuwe dingen kopen geeft altijd een korte kick, maar het zorgvuldig oppoetsen en het vervangen van kapotte onderdelen van een ding dat je dagelijks gebruikt voelt veel fijner. Hij is weer echt als nieuw. Op naar de volgende twee jaar.

Disney+’s algorithm still hasn’t figured out I’m binge watching all Marvel Cinematic Universe movies in chronological order. Luckily there is Wikipedia for the list.

Over Apple Workouts exporteren naar Strava

Als een mens langs een kanaal hardloopt, maar hij heeft geen hardloophorloge om, bestaat die hardloopsessie dan wel?

Sinds ik een nieuwe iPhone heb doet de Strava-app voor mijn Apple Watch het niet meer. Ik heb 'm al diverse keren opnieuw geïnstalleerd, ingelogd, uitgelogd, opnieuw gekoppeld, maar hij blijft in hetzelfde "meld je aan!"-scherm hangen. Anderhalve week geleden ben ik daardoor hartslaggegevens 'verloren' tijdens een 5k wedstrijd hier in de buurt. (Had ik maar meer moeten trainen, was ik er eerder achter gekomen.) Ik dacht dat ik het gefixt had, maar toen ik net weer een rondje wilde maken bleek hij nog altijd in die stand te zitten.

Dus ik terug naar huis.

Ik ben misschien een beetje te obsessief met het meten van mijn hardlooprondjes, maar ik wil ze gewoon opslaan. Hardlooprondjes is momenteel het enige data-type dat ik nog niet op mijn weblog post (al zou ik ze als privéposts houden denk ik). Maar ik deel ze allemaal braaf op en met de site Strava.

Strava is een hele fijne site om achteraf naar je hardloopdata te kijken. Er zijn heel veel overzichtjes, mooie kaartjes, en als je ze extra geld geeft maken ze ook nog leuke grafiekjes voor je. Voor opnemen moet je echter totaal niet bij hen zijn: zelfs als de Apple Watch-app het had gedaan, doet hij nog altijd onder voor de software van de TomTom Runner 2 (RIP).

De Apple Watch heeft zelf dus ook een native app om workouts mee vast te leggen, maar Strava importeert dus niet vanaf de HealthKit app. Ik roep dus al een tijdje te pas en te onpas dat ik me nog eens ga verdiepen in Swift en programmeren voor de Apple Watch, om zelf dan maar een app te maken waarmee ik kan hardlopen zoals ik met de TomTom kon.

Maar, tijdens een vurige zoektocht over het internet, in hardloopkleding en al, vond ik HealthFit, een app waarmee je wél je Apple Workouts naar Strava kan overzetten. Ik heb er net een rondje mee hardgelopen en hoewel ik nog niet álle functies van de TomTom terugzie, komt dit toch wel verdraaid dichtbij. En het ziet er sowieso fancyer uit met die kleurtjes.

Bovendien heeft de app nog veel meer export-targets, waaronder iCloud Drive en e-mail. Dus nu zit ik met binary pattern-matching in Elixir te kijken wat ik allemaal met een .fit bestand kan aanvangen. Wie weet ooit nog eens hardlooprondjes hier!

Meer laden