DEV Community

loading...
Cover image for Creating a fluent library under PHP

Creating a fluent library under PHP

jorgecc profile image Jorge Castro ・4 min read

But what is fluent anyways?

Let's say the next example:

$obj=new Library();

$obj->method1();
$obj->method2();
$obj->endmethod();
Enter fullscreen mode Exit fullscreen mode

Sometimes we have a library where we should call one method after another. So, we could write this library like this one:

$obj=new Fluent1();
$obj->method1()->method2('some value')->endchain(); // that is fluent
Enter fullscreen mode Exit fullscreen mode

Or we could even ident as follow

$obj=new Fluent1();
$obj->method1()
    ->method2('some value')
    ->endchain(); // fluent too
Enter fullscreen mode Exit fullscreen mode

It doesn't work with any library but libraries that need to pass states each method or to chain processes.

Then, how I can create a fluent library?

There are two ways. Do you want to add logic or loop inside the methods?. if not, then let's use the first method.

Fluent 1.0

To create a fluent library, we need the next requisites:

  • store states. A field is more than enough.
  • Since we retain states then the class (and the methods) must not be static.
  • each method (but the end-of-the-chain method) must return the same instance.

Let's say this class:

class Fluent1
{
    private $state;

    public function __construct($state=0)
    {
        $this->state = $state;
    }
    public function increase() {
        $this->state++;
        return $this;
    }
    public function decrease() {
        $this->state--;
        return $this;
    }
    public function show() {
        echo "The state is :".$this->state."<br>"
    }
}
Enter fullscreen mode Exit fullscreen mode

We have the state

private $state;
Enter fullscreen mode Exit fullscreen mode

And it could be any value or even be a collection of states. It's up to us. We are free to use any but we must store the information somewhere.

And we have the methods that return the same instance

public function decrease() {
    $this->state--;
    return $this; // <--- it is the true
}
Enter fullscreen mode Exit fullscreen mode

The method returns the same instance using return $this.

  • Q: But is it expensive cause we are returning the whole object?
  • A: Not really, we are not returning a copy (clone) of the object but the instance. When we return $this, we are returning something like a pointer (such as #AF54043404 i.e. an int64 (and some other information). It is quite cheap and we are not using a lot of memory but a couple of bytes.

Then, we could call the previous library as follow:

$obj=new Fluent1();
$obj->increase()
    ->increase()
    ->decrease()
    ->decrease()
    ->show();
Enter fullscreen mode Exit fullscreen mode

We could even generate a fluent setter.

class Fluent1
{
        // ....
    public function setState($state)
    {
        $this->state = $state;
        return $this;
    }
}
Enter fullscreen mode Exit fullscreen mode
$obj=new Fluent1();
$obj->setState(20)
        ->increase()
    ->increase()
    ->decrease()
    ->decrease()
    ->show();
Enter fullscreen mode Exit fullscreen mode

Fluent 2.0

Now, this is tricky. Sometimes we need something more advanced, for example, to have loops and branches.

What we could do:

  • the same methods of the first Fluent
  • if and loops
  • alter past methods. For example: ->set('Hello World')->uppercase() where uppercase alters the result of previous set() method.

To create a fluent library 2.0, we need the next requisites:

  • store states. A field is more than enough.
  • We need to store each method and we need to store the current method.
  • Since we retain states then the class (and the methods) must not be static.
  • each method (but the end-of-the-chain method) must return the same instance.
  • The end-of-the-chain methods finally execute all the methods. So, the methods are executed once the last method is called.
$obj=new Fluent2();
$obj->if(true)
        ->increase()
    ->else()
        ->decrease()
    ->endif()
    ->show();
Enter fullscreen mode Exit fullscreen mode
class Fluent2
{
    private $state;

    private $chain;
    private $pos;
    private $currentIf=false;

    public function __construct($state=0)
    {
        $this->state = $state;
        $this->chain=[];
        $this->pos=-1;
    }   
    public function increase() {
        $this->chain[]=['op'=>'increase','arg'=>null];
        return $this;
    }
    public function decrease() {
        $this->chain[]=['op'=>'decrease','arg'=>null];
        return $this;
    }
    public function if($condition) {
        $this->chain[]=['op'=>'if','arg'=>$condition];
        return $this;
    }
    public function else() {
        $this->chain[]=['op'=>'else','arg'=>null];
        return $this;
    }
    public function endif() {
        $this->chain[]=['op'=>'endif','arg'=>null];
        return $this;
    }
    public function show() {
        $this->chain[]=['op'=>'show','arg'=>null];
        $this->runAll();
    }
    private function runAll() {
        $this->pos=-1;
        $numChain=count($this->chain);
        while($this->pos<$numChain-1) {
            $this->pos++;
            $op=$this->chain[$this->pos]['op'];
            $arg=$this->chain[$this->pos]['arg'];
            echo "running $op($arg)<br>"; // for debug
            switch ($op) {
                case 'increase';
                    $this->state++;
                    break;
                case 'decrease';
                    $this->state--;
                    break;
                case 'if';          
                    $this->currentIf=$arg;
                    if($arg===false) {
                        // if it is not true, then we jump to the next else or endif
                        $foundPos=-1;
                        for($i=$this->pos+1;$i<$numChain;$i++) {
                            if($this->chain[$i]['op']=='else' || $this->chain[$i]['op']=='endif') {
                                $foundPos=$i;
                                break;
                            }
                        }
                        if ($foundPos===-1) {
                            trigger_error("if without endif");
                            die(1);
                        }
                        $this->pos=$foundPos; // we jump to the next else or if
                    }
                    break;
                case 'else':
                    if($this->currentIf===true) {
                        // if is true then we jump to the next endif
                        $foundPos=-1;
                        for($i=$this->pos+1;$i<$numChain;$i++) {
                            if($this->chain[$i]['op']=='endif') {
                                $foundPos=$i;
                                break;
                            }
                        }
                        if ($foundPos===-1) {
                            trigger_error("else without endif");
                            die(1);
                        }
                        $this->pos=$foundPos; // we jump to the next else or if
                    }
                    break;
                case 'endif':
                    break;
                case 'show':
                    echo "The result is ".$this->state."<br>";
                    break;
                default:
                    trigger_error("method not defined");
                    die(1);
                    break;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Where each method only stores its intent

public function decrease() {
    $this->chain[]=['op'=>'decrease','arg'=>null];
    return $this;
}
Enter fullscreen mode Exit fullscreen mode

And later, all those intents are executed.

Discussion (1)

Collapse
jclaveau profile image
Jean Claveau

I like what you try to do but you are doing multiple things at once, which could lead to unmaintainable code.

For the lazy (deferred) behavior, you may be interested in a lib I wrote github.com/jclaveau/php-deferred-c...

Forem Open with the Forem app