Private class callbacks

Posted: January 10, 2016 in Programming

PHP can be considered as hybrid language, mostly OOP language with support for some of the functional programming features. Most praised functionality is possibility to work with closures.

There are unlimited possibilities how to exploit closures, here, I will show you how to use them to encapsulate class methods when you are implementing  method callbacks for any reason.

(*UPDATE: Idea from this article resulted with library: https://github.com/RunOpenCode/sax)

So, why class have method callbacks? Well, for various reasons, validation per example, forms in, now obsolete, Symfony 1.4 (http://symfony.com/legacy/doc/cookbook/1_2/en/conditional-validator):

class loginForm extends sfForm
{
  public function configure()
  {
    //  ... Configure widgets and validators ...
    $this->validatorSchema->setPostValidator(
      new sfValidatorCallback(array('callback' => array($this, 'checkPassword')))
    );
  }
 
  public function checkPassword($validator, $values)
  {
    // ... Validation logic
  }
}

Do you notice that method “checkPassword” is public? It has to be in order to be accessible callbacks.

I have used this example because it is most obvious one, there are tons of similar examples related to modern frameworks such as (but not limited to) Symfony2 and related components such as Forms and Validation. In general, if you do not know SF1, that method will be called when form object received post parameters, so form can be validated.

Important question: what is wrong with this implementation?

If some method is public, that means that it is part of the public API and it can be called without some predefined context, consider this:

$form = new loginForm();
$form->checkPassword();

Does this method invocation makes any sense? Well, no, in order for method “checkPassword” to be called, some execution context should be satisfied. Per example, SF1 handles that execution context, so calling this method manually does not make any sense.

So, ok – we will not call it? But that is bad practice, what should not be a public API, it should not be a public API.

We have to encapsulate somehow this callback.

Method 1 – use anonymous function

Consider example below:

class loginForm extends sfForm
{
  public function configure()
  {
    //  ... Configure widgets and validators ...
    $this->validatorSchema->setPostValidator(
      new sfValidatorCallback(function($validator, $values) {
          // ... Validation logic
      })
    );
  }
}

Ok, by using this solution, we get that encapsulation. But what is wrong with this implementation?

  1. Complexity – callback function can be complex, which can be nested in already in complex method (in our example “configure()”), so our code can be very unreadable – and we can get poor grade for code quality (if we are using, per example, some static code analysis tool, like, per example, Scrutinizer).
  2. Callback function is not aware of class instance context, that is, “$this” have different meaning in scope of callback function.

Now, second issue can be solved with \Closure::bind(), but first problem will still remain, our code in surrounding method can get really complex.

So this is kinda limited solution.

Method 2 – use anonymous function to redirect call to private/protected method

Consider idea below:

class loginForm extends sfForm
{
  public function configure()
  {
    //  ... Configure widgets and validators ...
    $this->validatorSchema->setPostValidator(
      new sfValidatorCallback(\Closure::bind(function($validator, $values) {
          $this->checkPassword($validator, $values);
      }, $this))
    );
  }

  private function checkPassword($validator, $values)
  {
    // ... Validation logic
  }
}

Ok, now we are getting somewhere… First, we use Closure::bind to provide to anonymous function pointer to class instance via ‘$this’ variable. Moreover, that anonymous function can access to all private/protected methods/properties of that class instance, so we can just redirect call to private/protected method.

And voila, we have encapsulated callback method of class instance.

Free example – Java like Sax parsing in PHP, encapsulated.

Do you know how to parse XML document with SAX parser in PHP? Here is official example: http://php.net/manual/en/example.xml-structure.php

Terrible, right? How about to have that in OOP style?

Here is the recipe for that, Java like SAX parsing class, which you have to inherit and implement abstract methods. Do note how callbacks are encapsulated.

<?php 
/*  
 * This file is part of the runopencode/sax, an RunOpenCode project.
 *
 * (c) 2016 RunOpenCode  
 *
 * For the full copyright and license information, please view the LICENSE  
 * file that was distributed with this source code.  
 */ 
abstract class AbstractSaxHandler {     
/**      
 * Parse XML content and get result.     
 *
 * @param string $xml XML document or path to XML document.      
 * @return mixed Parsing result.      
 */
public function parse($xml)     
{        
    $parser = xml_parser_create();         
    $this->onDocumentStart($parser, $xml);

        xml_set_element_handler(
            $parser,
            \Closure::bind(function($parser, $name, $attributes) {
                $this->onElementStart($parser, $name, $attributes);
            }, $this),
            \Closure::bind(function($parser, $name) {
                $this->onElementEnd($parser, $name);
            }, $this)
        );

        xml_set_character_data_handler(
            $parser,
            \Closure::bind(function($parser, $data) {
                $this->onElementData($parser, $data);
            }, $this));

        $this->doParse($parser, $xml);

        $this->onDocumentEnd($parser);

        xml_parser_free($parser);

        return $this->getResult();
    }

    /**
     * Document start handler, executed when parsing process started.
     *
     * @param resource $parser Parser handler.
     * @param string $xml XML content.
     */
    protected abstract function onDocumentStart($parser, $xml);

    /**
     * Element start handler, executed when XML tag is entered.
     *
     * @param resource $parser Parser handler.
     * @param string $name Tag name.
     * @param array $attributes Element attributes.
     */
    protected abstract function onElementStart($parser, $name, $attributes);

    /**
     * Element CDATA handler, executed when XML tag CDATA is parsed.
     *
     * @param resource $parser Parser handler.
     * @param string $data Element CDATA.
     */
    protected abstract function onElementData($parser, $data);

    /**
     * Element end handler, executed when XML tag is leaved.
     *
     * @param resource $parser Parser handler.
     * @param string $name Tag name.
     */
    protected abstract function onElementEnd($parser, $name);

    /**
     * Document end handler, executed when parsing process ended.
     *
     * @param resource $parser Parser handler.
     */
    protected abstract function onDocumentEnd($parser);

    /**
     * Parsing error handler.
     *
     * @param string $message Parsing error message.
     * @param int $code Error code.
     * @param int $lineno XML line number which caused error.
     */
    protected abstract function onParseError($message, $code, $lineno);

    /**
     * Considering that your handler processed XML document, this method will collect
     * parsing result. This method is called last and it will provide return value for
     * AbstractSaxHandler::parse($xml) method.
     *
     * @return mixed
     */
    protected abstract function getResult();

    /**
     * Parse path to XML document/string content.
     *
     * @param resource $parser Parser.
     */
    protected function doParse($parser, $xml)
    {
        if (file_exists($xml)) {

            if (!($file = fopen($xml, 'r'))) {
                throw new \RuntimeException(sprintf('XML document "%s" is not readable.', $xml));
            }

            while ($data = fread($file, 4096)) {
                xml_parse($parser, $data, feof($file)) or $this->onParseError(xml_error_string(xml_get_error_code($parser)), xml_get_error_code($parser), xml_get_current_line_number($parser));
            }
        } else {

            if (!xml_parse($parser, $xml)) {
                $this->onParseError(xml_error_string(xml_get_error_code($parser)), xml_get_error_code($parser), xml_get_current_line_number($parser));
            }
        }
    }

    /**
     * Shortcut to parser parsing of XML document.
     *
     * @param string $xml
     * @return mixed Parsing result.
     */
    public static function execute($xml)
    {
        $xmlHandler = new static();
        return $xmlHandler->parse($xml);
    }
}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s