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();

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

Or we could even ident as follow

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

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>"
    }
}

We have the state

private $state;

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
}

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();

We could even generate a fluent setter.

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

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();
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;
            }
        }
    }
}

Where each method only stores its intent

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

And later, all those intents are executed.

Posted on by:

jorgecc profile

Jorge Castro

@jorgecc

You are free to believe in whatever you want to, me too. So, stop preaching your religion, politics, or belief. Do you have facts? Then I will listen. Do you have a personal belief? Sorry but no.

Discussion

markdown guide
 

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...