extend or implement simplexml to avoid DRY

Viewed 193

Please check following class that I've create to build few XML frames..

class CommandBuilder
{
    public function __construct()
    {
        //
    }

    public function login($username, $password)
    {
        $frame = $this->frame();

        $command = $frame->addChild('command');
        $login = $command->addChild('login');
        $login->addChild('username', $username);
        $login->addChild('password', $password);
        $command->addChild('authKey', 'authkey');

        return $frame->asXML();
    }

    public function info($id)
    {
        $frame = $this->frame();

        $command = $frame->addChild('command');
        $login = $command->addChild('product');
        $login->addChild('id', $id);
        $command->addChild('authKey', 'authkey');

        return $frame->asXML();
    }

    protected function frame()
    {
        return new SimpleXMLElement(
            '<app/>'
        );
    }
}

What's the best way to avoid duplication of $frame->addChild('command') and $command->addChild('authKey', 'authkey') without changing order of elements?

Please help to improve the code. Thanks

4 Answers

How about something like this, where you create a separate builder class:

class CommandBuilder
{
  private $commandName;

  private $params = [];

  public function __construct( $commandName ) {
    $this->commandName = $commandName;
  }

  // convenience method, to allow for cleaner fluent interface usage
  public static function create( $commandName ) {
    return new self( $commandName );
  }

  public function addParam( $paramName, $paramValue ) {
    $this->params[] = [ 'name' => $paramName, 'value' => $paramValue ];

    return $this;
  }

  public function build() {
    $app = new SimpleXMLElement( '<app/>' );
    $commandContainer = $app->addChild( 'command' );
    $command = $commandContainer->addChild( $this->commandName );
    foreach( $this->params as $param ) {
      $command->addChild( $param[ 'name' ], $param[ 'value' ] );
    }
    $commandContainer->addChild( 'authKey', 'authKey' );

    return $app->asXML();
  }  
}

and then have a separate class that builds the specific application commands:

class AppCommands
{
  public function login( $username, $password ) {
    return CommandBuilder::create( 'login' )->addParam( 'username', $username )
                                            ->addParam( 'password', $password )
                                            ->build();
  }

  public function info( $id ) {
    return CommandBuilder::create( 'product' )->addParam( 'id', $id )
                                              ->build();
  }
}

Usage stays the same, except you instantiate AppCommands, in stead of CommandBuilder:

$ac = new AppCommands;

echo $ac->login( 'MyUsername', 'MyPassword' );
echo PHP_EOL;
echo $ac->info( 5 );

View this example on eval.in

If you wanted, you could of course also dynamically pass the authKey to the CommandBuilder instead of hard-coding it inside, with something like:

class CommandBuilder
{
  private $commandName;

  private $authKey;

  private $params = [];

  public function __construct( $commandName, $authKey ) {
    $this->commandName = $commandName;
    $this->authKey = $authKey;
  }

  public static function create( $commandName, $authKey ) {
    return new self( $commandName, $authKey );
  }

  /* ... */

  public function build() {

    /* ... */

    $commandContainer->addChild( 'authKey', $this->authKey );

    return $app->asXML();
  }

Although the code you have could be simplified, there are complications. The way I've done it moves the building of the frame into the frame method. Each of the other routines builds the basis of the <command> node and passes this on, the frame method then adds the authkey bit. This code would do the same - BUT it would have to be done on all frames...

class CommandBuilder
{
    public function __construct()
    {
        //
    }

    public function login($username, $password)
    {
        $command = new SimpleXMLElement('<command />');
        $login = $command->addChild('login');
        $login->addChild('username', $username);
        $login->addChild('password', $password);

        return $this->frame($command);
    }

    public function info($id)
    {
        $command = new SimpleXMLElement('<command />');
        $login = $command->addChild('product');
        $login->addChild('id', $id);

        return $this->frame($command);
    }

    protected function frame( $node )   {
        $node->addChild('authKey', 'authkey');
        $xml = new DOMDocument();
        $xml->loadXML('<app/>');
        // Convert SimpleXML to DOMDocument
        $fromDom = dom_import_simplexml($node);
        // Add in the $node passed in to the frame
        $xml->documentElement->appendChild($xml->importNode($fromDom, true));

        return $xml->saveXML();
    }
}

It looks like your dependencies aren't too mixed up so this is a fairly straightforward example where you can move this into a method of its own. Remember that in PHP, objects are passed by reference. That means that the object variable is actually just a memory pointer to object, and is in contrast to how scalar and array variables are passed by default (by value... aka no memory pointer).

The takeaway is that the object is always the same no matter where it is used as long as you do not clone it.

<?php
class CommandBuilder
{
    public function preBuild(\SimpleXMLElement $node)
    {
        $command = $node->addChild('command');
        $command->addChild('authKey', 'authkey');
    }
}

Now, instead of calling those two methods, you can simply call $this->preBuild($frame).

How about writing a "skeleton" method that accepts as parameter a callable, and executes it in the middle, like so. (A disadvantage is that now, the parameters, like $username and $password, are repeated.)

class CommandBuilder
{
    // ...

    private function createCommand(callable $cmdEnricher)
    {
        $frame = $this->frame();

        $command = $frame->addChild('command');
        $cmdEnricher($command);
        $command->addChild('authKey', 'authkey');

        return $frame->asXML();
    }

    public function login($username, $password)
    {
        return $this->createCommand(function ($command) use ($username, $password) {
            $login = $command->addChild('login');
            $login->addChild('username', $username);
            $login->addChild('password', $password);
        });
    }

    public function info($id)
    {
        return $this->createCommand(function ($command) use ($id) {
            $login = $command->addChild('product');
            $login->addChild('id', $id);
        });
    }

    // ...
}
Related