Skip to content

Instantly share code, notes, and snippets.

@zackdouglas
Created April 14, 2011 05:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zackdouglas/918964 to your computer and use it in GitHub Desktop.
Save zackdouglas/918964 to your computer and use it in GitHub Desktop.
A simple PHP logger for ease of use with the decorator design pattern. This contains the abstract base class which should be extended in the least implementing _BaseLogger::resolveMessage(). As an example, this includes a sample stream logger.
<?php
/*******************************************************************************
* Expandable Class
*
* Authors:: anthony.gallagher@wellspringworldwide.com
*
* Copyright:: Copyright 2009, Wellspring Worldwide, LLC Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Source: https://github.com/wellspringworldwide/PHP-ClassMixer/blob/master/Mixins.php
******************************************************************************/
/*******************************************************************************
* Enables the dynamic addition of new methods or expander classes with
* extra functionality after the class has been defined.
******************************************************************************/
class ExpandableClassException extends Exception {}
abstract class ExpandableClassMixin {
static protected $dynamic_methods = array();
static protected $dynamic_mixins = array();
/**
* Helper method to generate an argument string to eval()
*
* @param <type> $num_args
* @return <type>
*/
protected static function makeArgStr($num_args) {
$m_args = array();
for ($i = 0; $i < $num_args; $i++) {
$m_args[] = "\$args[$i]";
}
return implode(', ', $m_args);
}
protected static function getClassHierarchy($base_object) {
$ancestry = array();
$klass = is_object($base_object) ? get_class($base_object) : $base_object;
$ancestry[] = $klass;
while (($klass = get_parent_class($klass)) !== false) {
$ancestry[] = $klass;
}
return $ancestry;
}
/***************************************************************************
* Dynamic method registration
**************************************************************************/
/**
* Check if a method has been dynamically added to a class
* @param <type> $klass
* @param <type> $method
* @return <type>
*/
protected static function isMethodRegistered($klass, $method) {
if (!array_key_exists($klass, self::$dynamic_methods)) {
self::$dynamic_methods[$klass] = array();
}
return isset(self::$dynamic_methods[$klass][$method]);
}
/**
* Dynamically add a method from a class. Methods should be functions with
* a signature of:
* function NEW_ClassName__MethodName($obj, ...) { ... }
* where $obj is the receiver for the $this pointer.
* @param <type> $klass
* @param <type> $method
*/
public static function registerMethod($klass, $method) {
//The name of the function to be called
if (!self::isMethodRegistered($klass, $method)) {
$dynamic_method_name = "NEW_".$klass."__".$method;
self::$dynamic_methods[$klass][$method] = $dynamic_method_name;
}
}
/**
* Dynamically remove a method from a class
* @param <type> $klass
* @param <type> $method
*/
public static function unregisterMethod($klass, $method) {
if (self::isMethodRegistered($klass, $method)) {
unset(self::$dynamic_methods[$klass][$method]);
}
}
/***************************************************************************
* Dynamic mixin registration
**************************************************************************/
/**
* Get the expanders for a given class
*
* @param <type> $klass
* @return <type>
*/
public static function getExpanders($klass) {
if (!array_key_exists($klass, self::$dynamic_mixins)) {
self::$dynamic_mixins[$klass] = array();
}
return self::$dynamic_mixins[$klass];
}
/**
* Check if an expander mixin has been dynamically added to a class
*
* @param <type> $klass
* @param <type> $expander
* @return <type>
*/
public static function isExpanderRegistered($klass, $expander) {
return in_array($expander, self::getExpanders($klass));
}
/**
* Dynamically add a mixin to a class.
*
* @param <type> $klass
* @param <type> $expander
*/
public static function registerExpander($klass, $expander) {
//The name of the function to be called
if (!self::isExpanderRegistered($klass, $expander)) {
self::$dynamic_mixins[$klass][] = $expander;
}
}
/**
* Dynamically remove a mixin from a class
*
* @param <type> $klass
* @param <type> $expander
*/
public static function unregisterExpanders($klass, $expander) {
if (self::isExpanderRegistered($klass, $expander)) {
self::$dynamic_mixins[$klass] =
array_values(array_diff(self::$dynamic_mixins[$klass], array($expander)));
}
}
/**
* Get all dynamically added methods
*
* @param <type> $object_or_klass
* @param <type> $method
*/
public static function get_dynamic_class_methods($object_or_klass) {
$methods = array();
$klasses = self::getClassHierarchy($object_or_klass);
foreach ($klasses as $klass) {
//Add the dynamically added methods
$dyn_methods = isset(self::$dynamic_methods[$klass]) ? array_keys(self::$dynamic_methods[$klass]) : array();
$methods = array_merge($methods, $dyn_methods);
//Add the expander methods
$expanders = self::getExpanders($klass);
foreach ($expanders as $expander) {
//Get the methods of the expander class
$exp_methods = get_class_methods($expander);
$methods = array_merge($methods, $exp_methods);
}
}
return $methods;
}
/**
* Find where a dynamic method for a class is registered
*
* @param <type> $object_or_klass
* @param <type> $method
* @return <type>
*/
public static function find_dynamic_class_method($object_or_klass, $method) {
$klasses = self::getClassHierarchy($object_or_klass);
foreach ($klasses as $klass) {
//Return the dynamic method if found
if (self::isMethodRegistered($klass, $method)) {
return array(null, "NEW_".$klass."__".$method);
}
//Get the expanders for the class
$expanders = self::getExpanders($klass);
foreach ($expanders as $expander) {
//Get the methods of the expander class
$methods = get_class_methods($expander);
//Found the method!
if (in_array($method, $methods)) {
return array($expander, $method);
}
}
}
return null;
}
/**
* Checks if a class has the method dynamically added
*
* @param <type> $object_or_klass
* @param <type> $method
* @return <type>
*/
public static function dynamic_method_exists($object_or_klass, $method) {
return is_array(self::find_dynamic_class_method($object_or_klass, $method));
}
/**
* Here look for expander classes that may define the method called on the object
* When using this mixin, the combinator for this method should be rerouted to
* the resulting class's __call method
*
* @param <type> $method
* @param <type> $args
* @return <type>
*/
public function ___call($method, $args) {
//Get the list of classes we need to check for expander registration
$klasses = self::getClassHierarchy($this);
foreach ($klasses as $klass) {
//Check if this is a dynamically added method, if so call the method
if (self::isMethodRegistered($klass, $method)) {
//A dynamically added method
array_unshift($args, $this);
$dynamic_method_name = "NEW_".$klass."__".$method;
eval("\$result =& $dynamic_method_name(".self::makeArgStr(count($args)).");");
return $result;
}
//Get the expanders for the class
$expanders = self::getExpanders($klass);
foreach ($expanders as $expander) {
//Get the methods of the expander class
$methods = get_class_methods($expander);
if (is_null($methods) && !class_exists($expander)) {
throw new Exception("Expander for `$klass` - `$expander` - is not a valid class. Please check the expander registration.");
}
//Found the method! Call it and return the result!
if (in_array($method, $methods)) {
eval("\$result =& $expander::$method(".self::makeArgStr(count($args)).");");
return $result;
}
}
}
//The method was not found...Trigger an exception.
throw new ExpandableClassException('Call to undefined method '.get_class($this).'::'.$method);
}
}
/**
* Just a pseudonym for the ExpandableClassMixin that we can inherit from
*/
abstract class ExpandableClass extends ExpandableClassMixin {
public function __call($method, $args) {
return parent::___call($method, $args);
}
}
<?php
/**
* Copyright 2011 Zack Douglas <zackery.douglas@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* defines an interface and basic methods for implementing a decorator-pattern
* logging framework.
*
* In this implementation, we are using the ExpandableClass as our base,
* derived from
* https://github.com/wellspringworldwide/PHP-ClassMixer
* but this may just as easily extend from stdClass.
*
* As an example usage:
* $logger = new FooLogger(E_ERROR);
* $logger->persist_args = true;
* $logger = new BarLogger(array('para1'=>'val1'), E_WARNING, $logger);
* $logger = new BazLogger('some_argument', E_NOTICE, $logger);
* // more decorations?!
* $logger->log(E_WARNING, 'My Warning');
* try {
* throw new ExampleError('My Error');
* } catch (ExampleError $e) {
* // in this example all loggers in the decoration chain will fire, as
* // logError() defaults to E_ERROR unless specified as the second argument
* $logger->logError($e);
* }
* NOTICE that we use the E_* constants, not LOG_*; this is INTENTIONAL. As of
* PHP 5.3.0, LOG_* is DEPRECATED, as well as being documented as part of the
* Network service
*/
abstract class _BaseLogger extends ExpandableClass {
/**
* the integer log level. A level less than zero indicates logging all.
*/
var $lvl = -1,
/**
* indicates that the logger should resolve its errors while executing,
* rather than with an explicit call to resolve(),
* or at time of destruction if $on_destruct is true
*/
$synchronized = false,
/**
* indicates whether the logger should implicitly resolve() when
* garbage collected
*/
$resolve_on_destruct = true,
/**
* an optional wrapped logger which should be notified of all events when
* this logger is triggered
*/
$wrapped = null,
/**
* queue of messages received from event methods
*/
$messages = array(),
/**
* persists certain arguments (synchronized, on_destruct) set on this
* logger to all decorating loggers. Default is false.
*/
$persist_args = false;
/**
* constructs a logger with minimal effort. You may optionally define both a
* logging level and a wrapped logger. By default, new loggers will listen
* to all messages; this is by design.
* @param int $lvl the logging level this logger will record
* @param _BaseLogger $wrapped the logger which we're decorating
*/
public function __construct($lvl=-1, _BaseLogger $wrapped=null) {
$this->lvl = $lvl;
$this->wrapped = $wrapped;
if ($wrapped->persist_args) {
$this->persist_args = true;
$this->synchronized = $wrapped->synchronized;
$this->resolve_on_destruct = $wrapped->resolve_on_destruct;
}
}
/**
* is fired when the logger loses scope and is garbage collected. If
* on_destruct is true, the logger chain will be resolve()'d
*/
public function __destruct() {
if ($this->resolve_on_destruct) {
$this->resolve();
}
}
/**
* provides a public method which can be called from decorating loggers on
* decorated loggers to store new messages
* @param int $lvl the indicator level
* @param str $msg the message to record
*/
public function log($lvl, $msg) {
$this->storeConditional($lvl, $msg);
if ($this->wrapped) {
$this->wrapped->log($lvl, $msg);
}
if ($this->synchronized) {
$this->resolve();
}
}
/**
* is meant to be used in a try-catch context where an Exception is raised
* but could be recovered from. This collects the message, file, and line
* number from the Exception argument
* @param Exception $e the exception to intercept and record
* @param int $lvl an optional level override, defaulting to E_ERROR
*/
public function logError(Exception $e, $lvl=E_ERROR) {
$cls = get_class($e);
$msg = "{$cls}: {$e->getMessage()} (Code {$e->getCode()})on line {$e->getLine()} of file {$e->getFile()}\nFULL TRACE:\n{$e->getTraceAsString()}";
$this->log($lvl, $msg);
}
/**
* determines whether to record the $msg based upon the internal logging
* level and the provided level of the $msg
* @param int $lvl the indicator level
* @param str $msg the message to record
*/
protected function storeConditional($lvl, $msg) {
$lvl_text = $this->getLogLevelText($lvl);
$dt = date('c');
if ($this->lvl >= $lvl || $this->lvl < 0) {
$this->messages []= "[ {$lvl_txt} ] {$dt} {$msg}";
}
}
/**
* provides a public method which can be called from decorating loggers to
* the decorated loggers to resolve their messages using resolveMessage().
* Messages are removed as they are individually resolved, allowing a
* modicum of disaster recovery.
*/
public function resolve() {
$message_keys = array_keys($this->messages);
foreach ($message_keys as $key) {
$this->resolveMessage($this->messages[$key]);
unset ($this->messages[$key]);
}
if ($this->wrapped) {
$this->wrapped->resolve();
}
}
/**
* resolves an individual message for the current logger
* @param str $msg the message to record
*/
protected function resolveMessage($msg) {
throw new NotImplementedException(__class__, __function__);
}
/**
* nullifies all recorded messages in the logger chain
*/
public function nullify() {
$this->messages = array();
if ($this->wrapper) {
$this->wrapper->nullify();
}
}
/**
* from an integer log level, retrieve the corresponding human-readable text
* @param int $lvl the indicator level
*/
public function getLogLevelText($lvl) {
if (E_ERROR === $lvl) {
return 'ERROR';
} elseif (E_WARNING === $lvl) {
return 'WARNING';
} elseif (E_PARSE === $lvl) {
return 'PARSE';
} elseif (E_NOTICE === $lvl) {
return 'NOTICE';
} elseif (E_STRICT === $lvl) {
return 'STRICT';
} else {
return 'DEBUG';
}
}
}
<?php
/**
* Copyright 2011 Zack Douglas <zackery.douglas@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* records log messages to a file or stream, delimited (by default) by the
* PHP_EOL constant.
*
* As an example usage:
* $logger = new StreamLogger(fopen('php://stderr', 'w'), E_WARN);
* // possible decorating loggers
* try {
* $logger->log(E_WARNING, 'Example warning');
* throw new ExampleError('Example error');
* } catch (ExampleError $e) {
* $logger->logError($e); // default level is E_ERROR
* }
*/
class StreamLogger extends _BaseLogger {
/**
* handle to a stream for writing
*/
var $stream = null,
/**
* whether this instance created a PHP stream
*/
$stream_created = false,
/**
* default message delimiter to the stream
*/
$delimiter = PHP_EOL;
/**
* from a location or stream, initialize parameters for logging to a stream
* @param str|stream $location_or_stream if a string, a new stream will be
* attempted to be opened for binary safe writing
* @param int $lvl the logging level
* @param _BaseLogger $wrapped an optional logger to decorate
*/
public function __construct($location_or_stream, $lvl, _BaseLogger $wrapped=null) {
if (is_string($location_or_stream)) {
$this->stream = @fopen($location_or_stream, 'ab');
if (!$this->stream) {
throw new RuntimeException("Could not open stream to {$location_or_stream} for appending");
}
$this->stream_created = true;
} else {
$meta = stream_get_meta_data($location_or_stream);
if (false === strpos($meta['mode'], 'a')
||
false === strpos($meta['mode'], 'w')
) {
throw new RuntimeException("Stream provided not available for writing");
}
$this->stream = $location_or_handle;
}
parent::__construct($lvl, $wrapped);
}
/**
* cleans up freshly allocated streams on garbage collection
*/
public function __destruct() {
if ($this->stream_created) {
fclose($this->stream);
}
}
/**
* records an individual message to the stream
* @param str $msg the log message to resolve
*/
protected function resolveMessage($msg) {
fwrite($this->stream, "{$msg}{$this->delimiter}");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment