We, PHP Programmers, usually writes a class with its required predefined attributes, getter/setter properties and use them inside our application. That’s cool enough. But, what happens when there is data, which isn’t quite stable/missing some attribute often, or sometimes might have several more attributes that we don’t know/require. If you have worked with various API like services, you might be familiar with such situations. Well, you might think for such situations, PHP JSON data might be a good choice. Now what about a combination of both, wouldn’t that be awesome, where some stable attributes are defined strictly and still allows to contain other dynamic data as well? Yep, we will achieve this in today’s tutorial and we will improve incrementally based on necessity, so that you can understand better what is done for which reason. Sit tight 😉 !
Basic PHP Class with attribute definition:
Lets write a simple class with few predefined attribute/properties first:
class MyObj
{
private $name;
private $occupation;
public function getName() {
return $this->name;
}
public function getOccupation() {
return $this->occupation;
}
public function setName($name) {
$this->name = $name;
}
public function setOccupation($occupation) {
$this->occupation = $occupation;
}
public function __construct($name = "", $occupation = "")
{
parent::__construct();
$this->name = $name;
$this->occupation = $occupation;
}
}
Code language: PHP (php)
This is a sample standard class definition that we use in our day to day PHP development. Lets make it more capable!
Property Access Capability:
as this abilities are some kind of generic, lets start them in a separate class which our ‘MyObj’ will extend to inherit those features. Now, a standard PHP object does have property like access by default. That means, you can use it as below:
$obj = new MyObj();
$obj->country = "Bangladesh";
echo $obj->country;
Code language: PHP (php)
So, why we are trying to rediscover this ability?
Well, though the above usability is very much possible, the data isn’t in your hand and you won’t be able to sync this to other accessibility features we will discuss next. So, to facilitate that, we will use an internal data structure to represent the object which will be shared in a consistent way by all other PHP dynamic object access features we will discuss today.
now, while a property like access/assignment takes place on our object, we can catch those with use of few magic methods as below:
class PDAO
{
/**
* @var array data
*/
protected $data = array();
/**
* Checks whether a key is set on this record.
*
* @param string $key The key.
* @return bool
*/
public function __isset($key)
{
return isset($this->data[$key]);
}
/**
* Get the value of a key in this record.
*
* @param string $key The key.
* @return mixed
*/
public function __get($key)
{
if(isset($this->data[$key]))
{
return $this->data[$key];
}
else
{
return null;
}
}
/**
* Set the value of a key in this record.
*
* @param string $key The key.
* @param string $value The value.
*/
public function __set($key, $value)
{
if ($key === null) {
$this->data[] = $value;
} else {
$this->data[$key] = $value;
}
}
/**
* Unset a property in this record
* @param type $name
*/
public function __unset($name)
{
if(isset($this->data[$name]))
{
unset($this->data[$name]);
}
}
}
Code language: PHP (php)
to verify whether data is set properly set to $data variable, you can try with a simple method implementation:
public function getData()
{
return $this->data;
}
Code language: PHP (php)
now do the check:
print_r($obj->getData());
Code language: PHP (php)
Adding Array Like Access Capability:
To add array like access capability, we will have to implement the ArrayAccess interface provided by PHP and write all the methods meaningfully with our existing ‘$data’ variable. Here is how we can do it:
class Record implements \ArrayAccess
{
//Other already written methods etc
/**
* Check if a key exist
* @param type $offset
* @return type
*/
public function offsetExists($offset)
{
return $this->__isset($offset);
}
/**
* get value for a key
* @param type $offset
* @return type
*/
public function offsetGet($offset)
{
return $this->__get($offset);
}
/**
* set value for a key
* @param type $offset
* @param type $data
* @return type
*/
public function offsetSet($offset, $data)
{
$this->__set($offset, $data);
}
/**
* unset a key
* @param type $offset
*/
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
}
Code language: PHP (php)
Now we will be able to use our object as below:
$obj["city"] = "Montreal";
echo $obj["city"];
Code language: PHP (php)
Multi-Dimensional Array Access:
Well, as we now can use our object as an array, but can we access it like a multidimensional array?Lets Try it:
$obj["skills"]["programming"] = "PHP";
Code language: PHP (php)
Oh! We fail!ed And an error like follows should occur:
Notice: Indirect modification of overloaded element of MyObj has no effect in {path-to-file}.php
Code language: HTTP (http)
Hmm, seems like our job isn’t complete yet. We have to handle it for situation like that. Luckily, it isn’t very hard. So, let’s do it:
/**
* set value for a key
* @param type $offset
* @param type $data
* @return type
*/
public function offsetSet($offset, $data)
{
// This check is important!
if (is_array($data)) {
$data = new self($data);
}
$this->__set($offset, $data);
}
/**
* Performs cloning of current object
*/
public function __clone() {
foreach ($this->data as $key => $value) {
if ($value instanceof self) {
$this[$key] = clone $value;
}
}
}
Code language: PHP (php)
Additionally, if we want to pass multidimensional array in constructor, those also need to be treated like follows:
/** * Constructor - Create a new document, or load an existing one from data. * * @param array $doc Document array. */ public function __construct(array $doc = array()) { $this->data = $doc; foreach ($doc as $key => $value) { $this[$key] = $value; } }
Iterative Capability Of Our PHP Dynamic Object:
Now as its possible to treat our object as array, you will might think that, may be we can iterate it like array as well without any issue. That’s wrong. We will have to add this capability externally. First, implement the PHP ‘Iterator’ interface. Then write the methods as like below:
/**
* Reset index
* @return type
*/
function rewind() {
return reset($this->data);
}
/**
* Return current data
* @return type
*/
function current() {
return current($this->data);
}
/**
* Return current key
* @return type
*/
function key() {
return key($this->data);
}
/**
* Return next key
* @return type
*/
function next() {
return next($this->data);
}
/**
* checks whether current element is valid
* @return type
*/
function valid() {
return key($this->data) !== null;
}
Code language: PHP (php)
Access As Pure Array:
Well, so, now we have our object to treat like a pure PHP dynamic object which we can use like property, like array and also iterate through the object as well. However, though most of our work is done, in case you really want it as a pure array, it won’t give you that by default. No worries, a simple function can help us achieve that as well:
/**
* Get the internal data array for this object.
*
* @return array
*/
public function toArray()
{
$data = $this->data;
foreach ($data as $key => $value) {
if ($value instanceof self) {
$data[$key] = $value->toArray();
}
}
return $data;
}
Code language: PHP (php)
The Final class definition will look like something below:
<?php
class PDAO implements \ArrayAccess, \Iterator
{
/**
* @var array data
*/
protected $data = array();
/**
* Constructor - Create a new object, or load an existing one from data.
*
* @param array $doc data array.
*/
public function __construct(array $doc = array())
{
$this->data = $doc;
foreach ($doc as $key => $value) {
$this[$key] = $value;
}
}
/**
* Checks whether a key is set on this object.
*
* @param string $key The key.
* @return bool
*/
public function __isset($key)
{
return isset($this->data[$key]);
}
/**
* Get the value of a key in this object.
*
* @param string $key The key.
* @return mixed
*/
public function __get($key)
{
if(isset($this->data[$key]))
{
return $this->data[$key];
}
else
{
return null;
}
}
/**
* Set the value of a key in this object.
*
* @param string $key The key.
* @param string $value The value.
*/
public function __set($key, $value)
{
if ($key === null) {
$this->data[] = $value;
} else {
$this->data[$key] = $value;
}
}
/**
* Unset a property in this object
* @param type $name
*/
public function __unset($name)
{
if(isset($this->data[$name]))
{
unset($this->data[$name]);
}
}
/**
* Check if a key exist
* @param type $offset
* @return type
*/
public function offsetExists($offset)
{
return $this->__isset($offset);
}
/**
* get value for a key
* @param type $offset
* @return type
*/
public function offsetGet($offset)
{
return $this->__get($offset);
}
/**
* set value for a key
* @param type $offset
* @param type $data
* @return type
*/
public function offsetSet($offset, $data)
{
if (is_array($data)) {
$data = new self($data);
}
$this->__set($offset, $data);
}
/**
* unset a key
* @param type $offset
*/
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
/**
* Reset index
* @return type
*/
function rewind() {
return reset($this->data);
}
/**
* Return current data
* @return type
*/
function current() {
return current($this->data);
}
/**
* Return current key
* @return type
*/
function key() {
return key($this->data);
}
/**
* Return next key
* @return type
*/
function next() {
return next($this->data);
}
/**
* checks whether current element is valid
* @return type
*/
function valid() {
return key($this->data) !== null;
}
}
Code language: HTML, XML (xml)
So, finally we do get our PHP dynamic object with much flexibility that might ease in various situation like template, consume third-party API etc. Happy coding 🙂
Petah Piper says
Why not add this to Github/packigist?