Jump to content

Module for conditional expressions?


MarkE
 Share

Recommended Posts

I'm looking for an inputfield module that might allow the entry and evaluation of a conditional expression (following php syntax).

My use case is an admin function for writing pro-forma emails/letters where some components of the pro-forma are dependent on conditions determined by an admin user. The pro-forma is then cloned for use and the relevant components are included depending on the runtime value of the conditions. The conditions usually include hanna codes as the items to be compared. This is implemented in a parent-child structure, where the main mail body is in the parent and each child then has a condition (textarea) field and a body textarea field for the optional text relating to that condition.

As an interim solution on my dev machine, I am just using eval() to evaluate the conditions, but I really don't want to use this in the live environment. My idea is to use an approach similar to that for hanna codes to store the php and render it. This would be (somehow) wrapped in a new inputfield module (extending InputfieldTextarea?) with an evaluate() method that would return true or false as appropriate. It would be placed inside a try...except structure to catch syntax errors etc.

It seemed to me that this might be quite a useful utility module and that someone might have developed something similar, but I can't find anything. Does anyone have any pointers, or will I need to start from scratch? If the latter, then I'd appreciate some help along the way as I am a bit of a novice in these matters.

Link to comment
Share on other sites

I built the following function which seems to work (borrowing heavily from the Hanna code approach). Not sure if its worth building a module around it.

/**
 * @param $condition
 * @param $pageId - page id holding the condition
 * @return bool
 * @throws WireException
 */
function evaluate_condition($condition, $pageId) {
    if ($condition and $pageId) {
        bd($condition, 'raw expression');
        $condition = html_entity_decode(strip_tags($condition), ENT_QUOTES | ENT_XML1, 'UTF-8'); // need extra params to catch   so that it can be removed by str_replace
        bd($condition, 'orig expression decoded');
        $condition = str_replace(' ', ' ', $condition);
        $condition = str_replace(' ', '', $condition);

        $cachePath = wire('config')->paths->cache . 'MailCondition/';
        $name = 'condition' . $pageId;
        if(!is_dir($cachePath)) if(!wireMkdir($cachePath)) {
            throw new WireException("Unable to create cache path: $cachePath");
        }

        $file = $cachePath . $name . '.php';

        $code = 'if (' . $condition . ') {echo "Y";} else {echo"N";}';
        $openPHP = '<' . '?php';
        $firstLine = 'if(!defined("PROCESSWIRE")) die("no direct access");';

        if(substr($code, 0, strlen($openPHP)) !== $openPHP) {
            // prepend open PHP tag to code if not already present
            $code = "$openPHP\n$firstLine\n$code";
        } else {
            // otherwise insert our $firstLine security check
            $code = str_replace($openPHP, "$openPHP\n$firstLine\n", $code);
        }

        if(is_file($file) && file_get_contents($file) === $code) {
            // file already there and same as what's in the DB
        } else {
            // write new file or overwrite existing
            if(!file_put_contents($file, $code, LOCK_EX)) throw new WireException("Unable to write file: $file");
            if(wire('config')->chmodFile) chmod($file, octdec(wire('config')->chmodFile));
        }

        $t = new TemplateFile($file);
        try {
            $result = $t->render();
            return ($result == "Y");
        } catch (\Error $e) {
            throw new WireException("Error in condition - cannot evaluate");
        }
    } else {
        throw new WireException("Incomplete parameters (condition, pageId) supplied - cannot evaluate");
    }
}

 

Link to comment
Share on other sites

5 hours ago, MarkE said:

Not sure if its worth building a module around it.

IMHO it's almost always worth building a module:

  • You can share it across projects
  • You can fix bux and add features in a central place
  • You can easily share it with the community
  • You can add documentation easily via readme file

And it's really almost no extra effort:

hQ9m9mQ.gif

  • Like 1
Link to comment
Share on other sites

6 minutes ago, bernhard said:

And it's really almost no extra effort:

Maybe for whizzos like you @bernhard, but I have only ever built simple process modules, using your excellent tutorial, and have struggled to find clear instructions for building Inputfield modules. It looks like there also needs to be a Fieldtype module, but I'm not sure why, or what should go in each.

Link to comment
Share on other sites

19 minutes ago, MarkE said:

Maybe for whizzos like you @bernhard, but I have only ever built simple process modules, using your excellent tutorial, and have struggled to find clear instructions for building Inputfield modules. It looks like there also needs to be a Fieldtype module, but I'm not sure why, or what should go in each.

Sorry - didn't get that you are talking about Fieldtype/Inputfield modules! They are definitely not that easy to grasp! ? It also took me really long to understand how they work and what the important parts are... The problem is that it's quite easy once you know how things work, but it's quite hard to understand what's going on when and where when you look at the code of a finished module (like the events fieldtype that was intended to be a tutorial module...).

Having said that, I'm still not sure if you really need a fieldtype/inputfield module. Also a basic module comes with the benefits mentioned above and it's as simple as that:

class YourModule extends WireData implements Module {

  public static function getModuleInfo() {
    return [
      'title' => 'YourModule',
      'version' => '0.0.1',
      'summary' => 'Your module description',
    ];
  }

  function evaluate_condition($condition, $pageId) {
    // your code here
  }
}

Then you can call your function:

$modules->get('YourModule')->evaluate_condition('foo', 123);

That's very little effort for lots of benefits. Maybe you don't even need to create a full blown Inputfield/Fieldtype module. You could maybe just make it autoload and add a hook to your module:

class YourModule extends WireData implements Module {

  public static function getModuleInfo() {
    return [
      'title' => 'YourModule',
      'version' => '0.0.1',
      'summary' => 'Your module description',
      'autoload' => true, // added this
    ];
  }

  public function init() {
    $this->addHookAfter("Inputfield::render", function($event) {
      $event->return .= "<div>My modified Inputfield</div>";
    });
  }

  function evaluate_condition($condition, $pageId) {
    // your code here
  }
}

Or even better, since you always check the condition for a single page, attach the method to the page object:

class YourModule extends WireData implements Module {

  public static function getModuleInfo() {
    return [
      'title' => 'YourModule',
      'version' => '0.0.1',
      'summary' => 'Your module description',
      'autoload' => true,
    ];
  }

  public function init() {
    $this->addHookAfter("Page::evaluate_condition", $this, "evaluate_condition");
  }

  function evaluate_condition(HookEvent $event) {
    $page = $event->object;
    $condition = $event->arguments(0);

    // your code here
    // instead of return 'foo' use $event->return = 'foo'
  }
}

Then you can do this on any processwire $page

$page->evaluate_condition("foo");

PS: The boilerplate code for every module is always the same. It might look frightening at first sight, but using vscode snippets for example it's really just typing "pwmodule" and filling in the placeholders (like shown in the gif above). https://marketplace.visualstudio.com/items?itemName=baumrock.pwsnippets

  • Like 1
Link to comment
Share on other sites

Thanks for the ideas. I decided to use a class for the template (MailConditional) and put the method in there. This simplifies things a bit for me - just need to call $mailPage->evaluate_condition().

<?php
namespace ProcessWire;

class MailConditionalPage extends DefaultPage
{
    /**
     * Create a new MailConditional page in memory.
     *
     * @param Template $tpl Template object this page should use.
     */
    public function __construct(Template $tpl = null) {
        if(is_null($tpl)) $tpl = $this->templates->get('MailConditionalPage');
        parent::__construct($tpl);
    }

    /**
     * @param $codes
     * @return bool
     * @throws WireException
     */
    public function evaluate_condition($codes=[]) {
...
}

 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...