Created
April 14, 2011 05:52
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/******************************************************************************* | |
* Expandable Class | |
* | |
* Authors:: [email protected] | |
* | |
* 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Copyright 2011 Zack Douglas <[email protected]> | |
* | |
* 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'; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Copyright 2011 Zack Douglas <[email protected]> | |
* | |
* 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