Editors Note, February 14th 2022: This project is now ABANDONED and no longer supported or updated.

I am proud to announce the addition of ‘features’ to asherwunk/phabstractic, particularly the Configuration feature.

Features (Mixins)

I’ve done some research, and I haven’t really found this as a type of pattern.  However, I did find something close to what I’ve done on Wikipedia:

In object-oriented programming languages, a mixin is a class that contains a combination of methods from other classes. How such a combination is done depends on the language, but it is not by inheritance.[citation needed] If a combination contains all methods of combined classes, it is equivalent to multiple inheritance.

Mixins encourage code reuse and can be used to avoid the inheritance ambiguity that multiple inheritance can cause [1] (the “diamond problem“), or to work around lack of support for multiple inheritance in a language.

The thing about PHP and the mixins I’ve developed is that they are not necessarily a class, instead, the idea is that they are a trait that offers a particular feature to another class.  So you define a feature, something the class can do, such as logging, or being able to hold configurations (see below), and then you can add it to a particular class.

For example, say you want some of your classes to be able to be configured and store configurations.  You could have a base class that includes that feature and then, in a single inheritance system like PHP, have all classes inherit from that class so that they can contain default behaviors.  The problem with this is that you end up implementing all the class ‘features’ on the base class creating bloat and unneeded complexity in the inheriting classes, particularly if only a subset of the classes inheriting need the feature.

This is where mixins can be handy.  So, following our configuration example, I can implement a configuration system in a  trait, and then any particular class that needs to be able to be configured in a standard fashion can use that trait, and implement the corresponding interface.  As well, when a subclass uses the feature ‘again’ (it is used in the parent class as well) the scope of the configuration stays with the class.  Suddenly we have only the classes that need that feature actually using that feature, and on top of that if we so choose we can re-implement the configuration functionality in a  different way (keeping the same interface of course).

Configuration

As it will be, I have implemented a configuration ‘feature’.  This class depends on the ZendConfig module of the Zend Framework (version 2.0 as noted in the composer file).  This also depends on ZendStdlib and ZendJson, so they are included as well.  The idea is that a class that uses this feature will gain a protected property $conf that stores all the configuration information.  I have implemented this as a ZendConfig object.  This has offered me some very useful functionality, as ZendConfig has ReadersWriters, and Processors for many various standards (such as JSON, YAML, arrays, and ini files).  As well, because the class tests for the interfaces provided by Zend/Config, you can implement further readers, writers, and processors for your own purposes.  I am planning to implement a database reader and writer.

You use the class by including the trait in your class.  This gives you the following public methods: configure(), saveSettings(), getSettings(), and processSettings().  Below is the undocumented code for $object->configure():

<?php

namespace Phabstractic\Features
{
	// ...

	$includes = array('/Features/Exception/ClassDependencyException.php',
		'/Features/Exception/InvalidArgumentException.php',
		'/Features/Resource/ConfigurationInterface.php',);

	// ...

	use Phabstractic\Features\Exception;
	use Phabstractic\Features\Resource as FeaturesResource;
	use Zend\Config;

	trait ConfigurationTrait
	{

		protected $conf;

		public function configure($configuration, $format = null, $context = null)
		{
			/* If configuration is an instance of a Zend\Config\Config
			   then we just use it and return */
			if ($configuration instanceof Config\Config) {
				$this->conf = $configuration;
				return;
			}

			/* If you pass in an array with two elements, one of them with
			   the key '#confformat' you can specify configuration with a
			   formatted string in the key 'configuration'
			   
			   For YAML you must also pass in processing information as
			   the key '#confcontext' as per the Zend Config documentation */
			if (is_array($configuration)) {
				if (array_key_exists('#confformat', $configuration) &&
				    array_key_exists('configuration', $configuration)) {
					$format = $configuration['#confformat'];
					if ($format == 'yaml') {
						if (array_key_exists('#confcontext', $configuration)) {
							$context = $configuration['#confcontext'];
						} else {
							throw new Exception\ClassDependencyException(
								'Phabstractic\\Features\\ConfigurationTrait->configure: ' .
								'Reader Context Not Defined');
						}
					}
					$configuration = $configuration['configuration'];
				} else {
					$configuration = array_change_key_case($configuration);
				}
			}

			/* $configuration information as a string could be a filepath, or
			   configuration information given as a string ($format contains
			   the format information in this case) */
			if (is_string($configuration)) {

				// The standard format readers provided by Zend Framework
				$readers = array('ini' => '\\Zend\\Config\\Reader\\Ini',
				                 'xml' => '\\Zend\\Config\\Reader\\Xml',
				                 'json' => '\\Zend\\Config\\Reader\\Json',
				                 'yaml' => '\\Zend\\Config\\Reader\\Yaml',);
				/* This is IMPORTANT: use $configReaders property to override
				   and extend the standard readers.  For example, a MySQL reader
				   would require a property definition:
				   
				   private $configReaders = array();  <-- be sure to initialize
														  to array in definition
				   ...
				   $this->configReaders = array( 'mysql', 
												 '\\Qualified\\Name\\Reader'); */
				if (isset($this->configReaders)) {
					$readers = array_merge($readers, $this->configReaders);
				}

				// Automatically set $extension to filename extension
				$extension = $format ?
					$format :
					strtolower(pathinfo($configuration, PATHINFO_EXTENSION));
				// Instantiate the proper reader class
				if (class_exists($readers[$extension])) {
					if ( $context ) {
						$reader = new $readers[$extension]($context);
					} else {
						$reader = new $readers[$extension]();
					}
				} else {
					throw new Exception\ClassDependencyException(
						'Phabstractic\\Features\\ConfigurationTrait->configure: ' .
						'Reader Class Not Defined');
				}

				/* Convert $configuration to array, as expected below, from
				   the reader object (must implement ReaderInterface) */
				if ($reader instanceof Config\Reader\ReaderInterface) {
					$from = $format ? 'fromString' : 'fromFile';
					$configuration = $reader->$from($configuration);
				} else {
					throw new Exception\ClassDependencyException(
						'Phabstractic\\Features\\ConfigurationTrait->configure: ' .
						'$reader Does Not Implement Config\\Reader\\ReaderInterface');
				}

			}

			// Now that we're guaranteed an array, instantiate Config object
			if (is_array($configuration)) {
				$this->conf = new Config\Config($configuration, true);
			} else {
				// This error should be unreachable, thus it is not in the unit test
				throw new Exception\InvalidArgumentException(
					'Phabstractic\\Features\\ConfiguraitonTrait->configure: ' .
					'$configuration Not Array');
			}

			return;
		}

		public function saveSettings(
			$file,
			$writer = null,
			$exclusive = true,
			$context = null
		) {
			// ...
		}

		public function getSettings($format, $context = null)
		{
			// ...
		}

		public function processSettings($processor)
		{
			// ...
		}

	}

}

This method accepts either a filename, array, or Zend/Config object as the first parameter, and an optional format argument.  If you pass a filename the method automatically detects the format of the file from its extension.  So, loading XML from testconfig.json won’t work.  If you pass the format (as the extension such as ‘ini’) argument, then the $configure parameter is interpreted as the string to be passed into a Zend/Config object.

NOTE: There is also a customization option where you can override the classes used to read the configuration information by setting the class property ::configReaders.  The format for this array is ‘extension’ => ‘fully qualified class name’.  This array is merged with the standard array, so all original classes are maintained as well.  This is done on a per-class basis and is not inherited.

If you pass an array, or a Zend/Config object, it constructs/copies the appropriate object into $object->conf.  In this particular implementation, $object->conf will always be a Zend/Config object.

The undocumented code for $object->saveSettings() reads as follows:

public function saveSettings(
    $file,
    $writer = null,
    $exclusive = true,
    $context = null
) {
    if ($writer && $writer instanceof Config\Writer\WriterInterface) {
        $writer->toFile($file, $this->conf, $exclusive);
        return;
    }
    
    $writers = array('ini' => '\\Zend\\Config\\Writer\\Ini',
                     'xml' => '\\Zend\\Config\\Writer\\Xml',
                     'array' => '\\Zend\\Config\\Writer\\PhpArray',
                     'json' => '\\Zend\\Config\\Writer\\Json',
                     'yaml' => '\\Zend\\Config\\Writer\\Yaml',);

    if (isset($this->configWriters)) {
        $writers = array_merge($writers, $this->configWriters);
    }
    
    $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
    if (class_exists($writers[$extension])) {
        if ( $context ) {
            $writer = new $writers[$extension]($context);
        } else {
            $writer = new $writers[$extension]();
        }
    } else {
        throw new Exception\ClassDependencyException(
            'Phabstractic\\Features\\ConfigurationTrait->saveSettings: ' .
            'Writer Class Not Defined');
    }
    
    if ($writer instanceof Config\Writer\WriterInterface) {
        if (strpos($file, '#string') !== 0) {
            $writer->toFile($file, $this->conf, $exclusive);
        } else {
            return $writer->toString($this->conf);
        }
    } else {
        throw new Exception\ClassDependencyException(
            'Phabstractic\\Features\\ConfigurationTrait->saveSettings: ' .
            '$writer Doesn\'t Implement Config\\Writer\\WriterInterface');
    }
    
    return true;
}

As you can see we supply the filename, optional writer object, and a file exclusive lock parameter (for the Config\Writer\WriterInterface toFile() method.  This defaults to true, just as it does in the interface definition).  If we supply a writer object that is compatible with Config\Writer\WriterInterface, we take our $this->con Config\Config object and save it using the supplied object.  Otherwise, we take the extension of the file to be written automatically (this is the only way to determine the format to be written) and instantiate the correct Config\Writer instance.

NOTE: As in ::configure() you can override and add to the writers array by declaring an array property in the using class ::$configWriters.  This is optional.  The format is array( ‘extension’ => ‘writer object’ );

If we want a string representation we use a special filename: #string.(extension).  Otherwise, we take the file and write the information to it.  This file can be read in by the above ::configure() function, for instance, right after object instantiation.

The following methods provide some additional functionality:

public function getSettings($format, $context = null)
{
    if ($format == 'yaml') {
        $settings = $this->saveSettings(
            '#string.' . $format,
            null,
            true,
            $context
        );
    } else {
        $settings = $this->saveSettings('#string.' . $format);
    }
    
    if ($settings === true) {
        return false;
    } else {
        return $settings;
    }
    
}

public function processSettings($processor)
{
    if ($processor instanceof Config\Processor\ProcessorInterface) {
        $processor->process($this->conf);
    } else {
        throw new Exception\ClassDependencyException(
            'Phabstractic\\Features\\ConfiguraitonTrait->processSettings: ' .
            '$processor Doesn\'t Implement Config\\Processor\\ProcessorInterface');
    }
    
}

These two methods simply let you use a Zend/Config/Processor compliant object (you have to instantiate it yourself) to process the configuration of the class, and ::getSettings() is a shortcut to retrieving the object’s settings as a string in the desired format (must be in the same format as a file extension, e.g. ini, array, JSON, etc.)

NOTE: If you are using the YAML extension, you are required to either use the YAML PECL extension, or a separate external library.  The library access object is passed in through the $context argument for both ::configure() and ::saveSettings().  The Phabstractic library uses Spyc, included in the composer.json file, to accomplish it’s unit-testing, but you are free to supply any library you need.

View on GitHub

This is part of the Phabstractic Library.

photo credit: Gaming Setup via photopin (license)

Asher Wolfstein

Metaverse Resident

About the Author

A metaverse resident, you can find me on Second Life (kadar.talbot) and other online platforms. I write about my digital life, my musings, and my projects as a programmer, webmaster, artist, and game designer. (exist (be wunk) (use rational imagination) (import artist coder maker furry) (conditional (if (eq you asshole) (me (block you))))

View Articles