Property chaining and isset in configuration object [duplicate]
I couldn't find a question that was quite like mine, but if you can find one feel free to let me know..
I'm trying to figure out how to effectively create an neat configuration object.
I want the object (or a config manager of some sort) to be able to convert an array or INI file into a parent/child grouping of objects.
Eg:
$config_array = array (
'somecategory' => array ( 'key' => 'somevalue' ),
'anothercategory' => array ( 'key' => 'anothervalue', 'key2' => 'anothervalue2' ),
);
$config = new Configuration_Array( $config_array );
echo $config->somecategory->key; // prints: 'somevalue'
I have effectively done this with the following code:
class Configuration_Array extends Configuration
{
protected $_child_class = 'Configuration_Array';
protected $_objects = null;
protected $_config = null;
public function __construct( $config_array = null, $child_class = null ) {
if ( null !== $config_array ) {
$this->setConfig( $config_array );
}
if ( null !== $child_class ) {
$this->setChildClass( $child_class );
}
}
public function __get( $name ) {
$name = strtolower( $name );
if ( ! isset ( $this->_objects[ $name ] ) ) {
$this->_createObject( $name );
}
return $this->_objects[ $name ];
}
public function __isset( $name ) {
$name = strtolower( $name );
return ( ( isset ( $this->_objects[ $name ] ) ) or $this->_can_create_object( $name ) );
}
public function reset() {
$this->_objects = null;
}
public function toArray() {
if ( ! is_array ( $this->_config ) ) {
throw new Exception( 'No configuration has been set' );
}
return $this->_config;
}
public function setConfig( $config ) {
if ( null === $config ) {
return $this->reset();
}
if ( ! is_array ( $config ) ) {
throw new Exception( 'Configuration is not a valid array' );
}
$this->_config = $config;
}
public function loadConfig( $path ) {
if ( ! is_string ( $path ) ) {
throw new Exception( 'Configuration Path "' . $path . '" is not a valid string' );
}
if ( ! is_readable ( $path ) ) {
throw new Exception( 'Configuration file "' . $path . '" is not readable' );
}
$this->setConfig( include( $path ) );
}
public function setChildClass( $class_name ) {
if ( ! is_string ( $class_name ) ) {
throw new Exception( 'Configuration Child Class is not a valid string' );
}
if ( ! class_exists ( $class_name ) ) {
throw new Exception( 'Configuration Child Class does not exist' );
}
$this->_child_class = $class_name;
}
public function getChildClass() {
if ( ! isset ( $this->_child_class ) ) {
throw new Exception( 'Configuration Child Class has not been set' );
}
return $this->_child_class;
}
protected function _createObject( $name ) {
$name = strtolower( $name );
if ( ! isset ( $this->_config[ $name ] ) ) {
throw new Exception( 'No configuration has been set for object "' . $name . '"' );
}
$child_class = $this->getChildClass();
if ( is_array ( $this->_config[ $name ] ) ) {
$child = new $child_class( $this->_config[ $name ], $child_class );
} else {
$child = $this->_config[ $name ];
}
return ( $this->_objects[ $name ] = $child );
}
protected function _can_create_object( $name ) {
$name = strtolower( $name );
return isset ( $this->_config[ $name ] );
}
}
The Problem
Most of this works perfectly, but I am having some trouble figuring out how I can use isset effectively. With property chaining, isset only works on the last value in the chain, eg:
if ( isset ( $config->somecategory->key ) ) {
Which uses the object returned by $config->somecategory and checks whether it holds an object called 'key'
This means that if $config->somecategory doesn't exist, an exception is thrown. The user would have to do this to check effectively:
if ( isset ( $config->somecategory ) and isset ( $config->somecategory->key ) ) {
But that seems quite annoying.
An array on the other hand doesn't need to be checked at each level; PHP can check the entire thing:
if ( isset ( $config[ 'somecategory' ][ 'key' ] ) ) { // No error/exception
What I'm looking for is a way to implement my class so I can treat my objects sort of the same way I'd treat an array:
if ( isset ( $config->somecategory->key ) ) {
In a way that wouldn't throw an exception if 'somecategory' doesn't exist...
Ideas?
Since PHP 7 it's possible to use a not well documented feature of null coalesce operator for this purpose.
$config_array = [
'somecategory' => [ 'key' => 'somevalue' ],
'anothercategory' => [ 'key' => 'anothervalue', 'key2' => 'anothervalue2' ],
];
// Quickly convert to object
$json = json_encode($config_array);
$config = json_decode($json);
echo $config->somecategory->key ?? null; // prints: 'somevalue'
echo $config->somecategory->missing_key ?? null; // no errors but also doesn't print anything
echo $config->somecategory->missing_key->go->crazy->with->chains ?? null; // no errors but also doesn't print anything
Here is an online example in action
Unfortunately there is not version of isset
which checks your property chain correctly. Even writing your own method will not help as passing the chain as parameter to your method already fails if somecategory
is already unset.
You can implement the magic method for accessing unset properties (maybe in a base class common to your config objects). This will create a dummy object of class UnsetProperty
and return that.
Your class to $config->someCategory->key
will deliver a UnsetProperty
for $config->someCategory
. This object will also delivery a new UnsetProperty
for $obj->key
. If you implement a method IsSet()
in UnsetProperty
returning false and in other properties returning true you can simplyfy your check to:
if($config->someCategory->key->IsSet()) ...
This will need a lot of to do so I am not sure if you do not like to go with the chained isset-calls.
if((isset($config->someCategory)) and (isset($config->someCategory->key))) ...
Depends on style and how many nested levels you have.
Hope you get the idea behind the possibility.
Take a look at Zend_Config. It operates almost exactly as you describe.
You could use it directly or simply as an instructional guide to build your own.
Maybe something like this?
The only problem is, you would have to call isEmpty
to check if a configuration is given, and get
to get the final value. (Like can be seen in the 3 test cases at the bottom)
<?php
// set debug
error_reporting(E_ALL ^ E_STRICT);
ini_set('display_errors', 'on');
class Config
{
protected $data;
public function __construct($data = null) {
$this->data = $data;
}
public function __get($name) {
return isset($this->data[$name]) ? new Config($this->data[$name]) : new Config();
}
public function isEmpty() {
return empty($this->data);
}
public function get() {
return $this->data;
}
}
$test = new Config(array(
'foo' => array(
'bar' => array(
'barfoo' => 1
)
)
));
// test 1
if (!$test->foo->bar->isEmpty()) {
print_r($test->foo->bar->get());
}
// test 2
if (!$test->foo->bar->foobar->isEmpty()) {
print_r($test->foo->bar->foobar->get());
}
// test 3
if (!$test->foo->bar->barfoo->isEmpty()) {
print_r($test->foo->bar->barfoo->get());
}
Example:
http://codepad.org/9EZ2Hqf8
精彩评论