Friday, 15 February 2013

PHP and the Friday-brain


I'm not gonna lie to you. Today is messed up. This post will be hard to follow. I was working on a strange JSON crawler integration in Codeigniter. I had to map JSON object properties to DB fields in an array.

It's not an extraordinary process, looks like this:

$json_object = json_decode($json_string);
$mapping = array(
  'field1' => $json_object->field1,
  // ...
);


That's fine - however existence of keys in the object are not sure. So you need to check it in order to avoid complier errors:

$json_object = json_decode($json_string);
$mapping = array(
  'field1' => isset($json_object->field1) ? $json_object->field1 : NULL,
  // ...
);


Repeating this patter makes you goose bumps. Isset and the ternary operatior all the time screams for simplification. However one is important here: almost only isset can make is sure that a property exists. All right. So it's PHP, we cannot define macros, pity. No time either to write a Zend extension. Anonymous functions are useless too, compiler will kill you before it hits the anon function's body.

Then it hit me the idea: magic. Let's use magic methods. We can create a class that provides safe values for objects:

class safe {

  protected $data;

  function __construct($data = NULL) {
    $this->data = $data;

    if (is_object($data)) {
      foreach ($data as $key => $value) {
        if (is_object($data->{$key})) {
          $this->data->{$key} = new safe($data->{$key});
        }
      }
    }
  }

  function __get($name) {
    if (isset($this->data->{$name})) {
      return $this->data->{$name};
    }

    return NULL;
  }

}


Using the new satanic power the original code looks like this:

$json_object = new safe(json_decode($json_string));
$mapping = array(
  'field1' => $json_object->field1,
  // ...
);


Here comes the awkward: yeeeeaaah. It works, indeed. Although, I hate myself. What have we done? The loop in the constructor makes sure that nested values are safe too. The accessor provides the safe value return. Where is the problem here? Well, first it's not general. Only objects. Secondly it's not working over 2 level or undefined properties. Only the very first one. Why? Simply it's not possible to fake a magic object to be null as a value and act as an object otherwise.

Let's make a twist and think creatively. If a NULL cannot respond to any function calls let's try to avoid calling it before we check. The only way I've found is to separate the property path from the variable:

function safe_value($value, $keys = NULL) {
  if ($keys == NULL) {
    return $value;
  }
  elseif (!is_array($keys)) {
    return isset($value->{$keys}) ?
      $value->{$keys} : (
        isset($value[$keys]) ?
          $value[$keys] :
          NULL
      );
  }

  $key = array_shift($keys);
  return isset($value->{$key}) ?
    safe_value($value->{$key}, $keys) : (
      is_array($value) && isset($value[$key]) ?
        safe_value($value[$key], $keys) :
        NULL
    );
}


I know. But it's exactly what we needed. If that's the variable in question:

$json_object->field1->field2[1]->value;


then we don't have to do this:

isset($json_object->field1) && 
isset($json_object->field1->field2) && 
isset($json_object->field1->field2[1]) &&
isset($json_object->field1->field2[1]->value) ? 
  $json_object->field1->field2[1]->value : 
  NULL;


we can do this instead:

safe_value($json_object, array('field1', 'field2', 1, 'value'));


It's looking small, general, array and object compatible. But the night is not over yet. I made some tests and found a problem. If I use my magic converter on an object, like this:

$magic_object = new safe($json_object);


Then my safe_value() function is not working. It doesn't find the objects. I was investigating some time and it looked perfect, I could access the values by writing the direct version:

$json_object->field1->field2[1]->value;


but not the new version:

safe_value($json_object, array('field1', 'field2', 1, 'value'));


Any idea why? I let you think with the thinking image ...



Time is over. The culprit was the isset() call in safe_value(). When using __get() the compiler doesn't know by heart what is set - which is somewhat correct. You need to tell it explicit by adding __isset() to the safe() class:

  function __isset($name) {
    return isset($this->data->{$name});
  }


---

And now the world can move along. What's the lesson? Nothing particular, it's Friday. Go to sleep.

Peter

2 comments:

  1. This looks like overkill - you shouldn't need to go to that much trouble in PHP to find data in nested arrays. Have a look at this example, which runs with no warnings:

    https://gist.github.com/rupertj/5370574

    ReplyDelete
    Replies
    1. Oops. I meant to put that comment on your other article!

      Delete