浏览代码

First set of files:

- Base file, contains some common classes and functions for use in the
  framework.
- An autoloader class, to load PHP classes in a specific directory.
- A logger class with functionality similar to the Python built-in `logging'
  module.
- Unit tests for the files listed above.
- README and git/unit tests/documentation config files.
Taddeus Kroes 13 年之前
当前提交
34905f8bb3
共有 14 个文件被更改,包括 736 次插入0 次删除
  1. 3 0
      .gitignore
  2. 21 0
      README.txt
  3. 172 0
      autoloader.php
  4. 75 0
      base.php
  5. 167 0
      logger.php
  6. 15 0
      phpdoc.dist.xml
  7. 25 0
      phpunit.xml
  8. 5 0
      tests/_files/baz.php
  9. 5 0
      tests/_files/foo.php
  10. 7 0
      tests/_files/foo/bar.php
  11. 5 0
      tests/_files/second/foo_baz.php
  12. 96 0
      tests/test_autoloader.php
  13. 27 0
      tests/test_base.php
  14. 113 0
      tests/test_logger.php

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+*.swp
+*.~
+build/*

+ 21 - 0
README.txt

@@ -0,0 +1,21 @@
+-------
+Summary
+-------
+Minimalistic is a set of classes to create websites with. The core exists of
+a class autoloader, a template parser, a logger and some array manipulation
+functions. No MVC 'model' implementation is included, there are already many
+of these out there (PHPActiveRecord is recommended).
+
+----------
+Unit tests
+----------
+Unit tests are in the 'tests/' directory. PHPUnit is used to run tests. The
+PHP extension Xdebug needs to be installed in order to generate a code
+coverage report. To run unit tests, simply run 'phpunit' in the root
+directory.
+
+-------------
+Documentation
+-------------
+PhpDocumentor can be used to generate documentation in the 'docs/' directory.
+Just run 'phpdoc' in the root directory.

+ 172 - 0
autoloader.php

@@ -0,0 +1,172 @@
+<?php
+/**
+ * 
+ * 
+ * @author Taddeus Kroes
+ * @version 1.0
+ * @date 13-07-2012
+ */
+
+namespace Minimalistic;
+
+require_once 'base.php';
+
+/**
+ * Object that to automatically load classes within a root directory.
+ * 
+ * An Autoloader instance can register itself to the SPL autoload stack.
+ * 
+ * @package Minimalistic
+ */
+class Autoloader extends Base {
+	/**
+	 * The root directory to look in.
+	 * 
+	 * @var string
+	 */
+	private $root_directory;
+	
+	/**
+	 * The namespace classes in the root directory are expected to be in.
+	 * 
+	 * This namespace is removed from loaded class names.
+	 * 
+	 * @var string
+	 * @todo implement this
+	 */
+	private $root_namespace = '';
+	
+	/**
+	 * Whether to throw an exception when a class file does not exist.
+	 * 
+	 * @var bool
+	 */
+	private $throw_errors;
+	
+	/**
+	 * Create a new Autoloader instance.
+	 * 
+	 * @param string $directory Root directory of the autoloader.
+	 * @param bool $throw Whether to throw an exception when a class file does not exist.
+	 */
+	function __construct($directory, $throw=true) {
+		$this->set_root_directory($directory);
+		$this->set_throw_errors($throw);
+	}
+	
+	/**
+	 * Set whether to throw an exception when a class file does not exist.
+	 * 
+	 * @param bool $throw Whether to throw exceptions.
+	 */
+	function set_throw_errors($throw) {
+		$this->throw_errors = !!$throw;
+	}
+	
+	/**
+	 * Whether an exception is thrown when a class file does not exist.
+	 * 
+	 * @returns bool
+	 */
+	function get_throw_errors() {
+		return $this->throw_errors;
+	}
+	
+	/**
+	 * Set the root directory from which classes are loaded.
+	 * 
+	 * @param string $directory The new root directory.
+	 */
+	function set_root_directory($directory) {
+		$this->root_directory = self::path_with_slash($directory);
+	}
+	
+	/**
+	 * Get the root directory from which classes are loaded.
+	 * 
+	 * @returns string
+	 */
+	function get_root_directory() {
+		return $this->root_directory;
+	}
+	
+	/**
+	 * Append a slash ('/') to the given directory name, if it is not already there.
+	 * 
+	 * @param string $directory The directory to append a slash to.
+	 * @returns string
+	 */
+	static function path_with_slash($directory) {
+		return $directory[strlen($directory) - 1] == '/' ? $directory : $directory.'/';
+	}
+	
+	/**
+	 * Convert a class name to a file name.
+	 * 
+	 * Uppercase letters are converted to lowercase and prepended
+	 * by an underscore ('_').
+	 * 
+	 * @param string $classname The class name to convert.
+	 * @returns string
+	 */
+	static function classname_to_filename($classname) {
+		return strtolower(preg_replace('/(?<=.)([A-Z])/', '_\\1', $classname));
+	}
+	
+	/**
+	 * Create the path to a class file.
+	 * 
+	 * Any namespace prepended to the class name is split on '\', the
+	 * namespace levels are used to indicate directory names.
+	 * 
+	 * @param string $classname The name of the class to create the file path of.
+	 */
+	function create_path($classname) {
+		$namespaces = array_filter(explode('\\', $classname));
+		$dirs = array_map('self::classname_to_filename', $namespaces);
+		$path = $this->root_directory;
+		
+		if( count($dirs) > 1 )
+			$path .= implode('/', array_slice($dirs, 0, count($dirs) - 1)).'/';
+		
+		$path .= end($dirs).'.php';
+		return strtolower($path);
+	}
+	
+	/**
+	 * Load a class.
+	 * 
+	 * Any namespace prepended to the class name is split on '\', the
+	 * namespace levels are used to indicate directory names.
+	 * 
+	 * @param string $classname The name of the class to load, including pepended namespace.
+	 * @param bool $throw Whether to throw an exception if the class file does not exist.
+	 * @returns bool
+	 * @throws FileNotFoundError If the class file does not exist.
+	 */
+	function load_class($classname, $throw=true) {
+		$path = $this->create_path($classname);
+		
+		if( !file_exists($path) ) {
+			if( !$throw || !$this->throw_errors )
+				return false;
+			
+			throw new FileNotFoundError($path);
+		}
+		
+		require_once $path;
+		return true;
+	}
+	
+	/**
+	 * Register the autoloader object to the SPL autoload stack.
+	 * 
+	 * @param bool $prepend Whether to prepend the autoloader function to
+	 *                      the stack, instead of appending it.
+	 */
+	function register($prepend=false) {
+		spl_autoload_register(array($this, 'load_class'), true, $prepend);
+	}
+}
+
+?>

+ 75 - 0
base.php

@@ -0,0 +1,75 @@
+<?php
+/**
+ * Commonly used classes used in the Minimalistic package.
+ * 
+ * @author Taddeus Kroes
+ * @version 1.0
+ * @date 13-07-2012
+ * @package Minimalistic
+ */
+
+namespace Minimalistic;
+
+require_once 'logger.php';
+
+/**
+ * Base class for instantiable classes in the Minimalistic package.
+ * 
+ * The base class defines a static 'create' method that acts as a chainable
+ * shortcut for the class constructor.
+ * 
+ * @package Minimalistic
+ */
+abstract class Base {
+	/**
+	 * Create a new object of the called class.
+	 * 
+	 * This function provides a chainable constructor, which is not possible
+	 * using plain PHP code.
+	 * 
+	 * @returns mixed
+	 */
+	final static function create(/* [ arg0 [ , ... ] ] */) {
+		$args = func_get_args();
+		$class = get_called_class();
+		$rc = new \ReflectionClass($class);
+		
+		return $rc->newInstanceArgs($args);
+	}
+}
+
+/**
+ * Exception, thrown when a required file does not exist.
+ * 
+ * @package Minimalistic
+ */
+class FileNotFoundError extends \RuntimeException {
+	/**
+	 * Create a new FileNotFoundError instance.
+	 * 
+	 * Sets an error message of the form 'File "path/to/file.php" does not exist.'.
+	 * 
+	 * @param string $path Path to the file that does not exist.
+	 */
+	function __construct($path) {
+		$this->message = sprintf('File "%s" does not exist.', $path);
+	}
+}
+
+/**
+ * Format a string of the form 'foo %(bar)' with given parameters like array('bar' => 'some value').
+ * 
+ * @param string $format The string to format.
+ * @param array $params An associative array with parameters that are used in the format.
+ */
+function asprintf($format, array $params) {
+	return preg_replace_callback(
+		'/%\(([a-z-_ ]*)\)/i',
+		function ($matches) use ($params) {
+			return (string)$params[$matches[1]];
+		},
+		$format
+	);
+}
+
+?>

+ 167 - 0
logger.php

@@ -0,0 +1,167 @@
+<?php
+/**
+ * Logging functions.
+ * 
+ * @author Taddeus Kroes
+ * @version 1.0
+ * @date 13-07-2012
+ */
+
+namespace Minimalistic;
+
+/**
+ * Logger class. 
+ * 
+ * A Logger object provides five functions to process log messages.
+ * 
+ * @package Minimalistic
+ */
+class Logger {
+	const CRITICAL = 0;
+	const ERROR = 1;
+	const WARNING = 2;
+	const INFO = 3;
+	const DEBUG = 4;
+	static $level_names = array('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG');
+	
+	const DEFAULT_FORMAT = '%(datetime): %(level): %(message)';
+	
+	private $properties = array();
+	private $output = array();
+	private $format = self::DEFAULT_FORMAT;
+	private $level = self::WARNING;
+	
+	function set_format($format) {
+		$this->format = (string)$format;
+	}
+	
+	function get_format() {
+		return $this->format;
+	}
+	
+	function get_level() {
+		return $this->level;
+	}
+	
+	function get_level_name() {
+		return self::$level_names[$this->level];
+	}
+	
+	function set_level($level) {
+		if( is_string($level) ) {
+			$level = strtoupper($level);
+			
+			if( !defined('self::'.$level) )
+				throw new \InvalidArgumentException(sprintf('Invalid debug level %s.', $level));
+			
+			$level = constant('self::'.$level);
+		}
+		
+		if( $level < self::CRITICAL || $level > self::DEBUG )
+			throw new \InvalidArgumentException(sprintf('Invalid debug level %d.', $level));
+		
+		$this->level = $level;
+	}
+	
+	function set_property($name, $value) {
+		$this->properties[$name] = (string)$value;
+	}
+	
+	function critical($message) {
+		$this->process($message, self::CRITICAL);
+	}
+	
+	function error($message) {
+		$this->process($message, self::ERROR);
+	}
+	
+	function warning($message) {
+		$this->process($message, self::WARNING);
+	}
+	
+	function info($message) {
+		$this->process($message, self::INFO);
+	}
+	
+	function debug($message) {
+		$this->process($message, self::DEBUG);
+	}
+	
+	private function process($message, $level) {
+		if( $level <= $this->level )
+			$this->output[] = array($message, $level);
+	}
+	
+	function dumps() {
+		$logger = $this;
+		$output = '';
+		
+		foreach( $this->output as $i => $tuple ) {
+			list($message, $level) = $tuple;
+			$i && $output .= "\n";
+			$output .= preg_replace_callback(
+				'/%\(([a-z-_ ]*)\)/i',
+				function ($matches) use ($logger, $message, $level) {
+					$name = $matches[1];
+					
+					if( $name == 'message' )
+						return $message;
+					
+					if( $name == 'level' )
+						return Logger::$level_names[$level];
+					
+					return $logger->get_formatted_property($matches[1]);
+				},
+				$this->format
+			);
+		}
+		
+		return $output;
+	}
+	
+	function dump() {
+		echo $this->dumps();
+	}
+	
+	function clear() {
+		$this->output = array();
+	}
+	
+	function save($path) {
+		file_put_contents($path, $this->dumps());
+	}
+	
+	function handle_exception(\Exception $e) {
+		if( $e === null )
+			return;
+		
+		$message = sprintf("Uncaught %s in file %s, line %d: %s\n\n%s", get_class($e),
+			$e->getFile(), $e->getLine(), $e->getMessage(), $e->getTraceAsString());
+		$this->critical($message);
+		$this->dump();
+	}
+	
+	function set_as_exception_handler() {
+		set_exception_handler(array($this, 'handle_exception'));
+	}
+	
+	function get_formatted_property($property) {
+		if( isset($this->properties[$property]) )
+			return $this->properties[$property];
+		
+		switch( $property ) {
+			case 'loglevel':
+				return $this->get_level_name();
+			case 'date':
+				return strftime('%d-%m-%Y');
+			case 'time':
+				return strftime('%H:%M:%S');
+			case 'datetime':
+				return strftime('%d-%m-%Y %H:%M:%S');
+		}
+		
+		throw new \InvalidArgumentException(sprintf('Invalid logging property "%s".', $property));
+	}
+}
+
+?>

+ 15 - 0
phpdoc.dist.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<phpdoc>
+	<title>Minimalistic documentation</title>
+	<parser>
+		<default-package-name>Minimalistic</default-package-name>
+		<target>build/docs</target>
+	</parser>
+	<transformer>
+		<target>build/docs</target>
+	</transformer>
+	<files>
+		<directory>.</directory>
+		<ignore>tests/*</ignore>
+	</files>
+</phpdoc>

+ 25 - 0
phpunit.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit colors="false"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         stopOnError="true">
+	<testsuites>
+		<testsuite name="Minimalistic test suite">
+			<directory prefix="test_" suffix=".php">tests</directory>
+		</testsuite>
+	</testsuites>
+	
+	<logging>
+		<log type="coverage-html" target="build/coverage" charset="UTF-8" highlight="true" />
+	</logging>
+	
+	<filter>
+		<whitelist>
+			<directory>.</directory>
+			<exclude>
+				<directory>tests</directory>
+			</exclude>
+		</whitelist>
+	</filter>
+</phpunit>

+ 5 - 0
tests/_files/baz.php

@@ -0,0 +1,5 @@
+<?php
+
+class Baz {}
+
+?>

+ 5 - 0
tests/_files/foo.php

@@ -0,0 +1,5 @@
+<?php
+
+class Foo {}
+
+?>

+ 7 - 0
tests/_files/foo/bar.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Foo;
+
+class Bar {}
+
+?>

+ 5 - 0
tests/_files/second/foo_baz.php

@@ -0,0 +1,5 @@
+<?php
+
+class FooBaz extends Foo {}
+
+?>

+ 96 - 0
tests/test_autoloader.php

@@ -0,0 +1,96 @@
+<?php
+
+require_once 'autoloader.php';
+use Minimalistic\Autoloader;
+
+define('PATH', 'tests/_files/');
+
+class AutoloaderTest extends PHPUnit_Framework_TestCase {
+	var $autoloader;
+	
+	function setUp() {
+		$this->autoloader = new Autoloader(PATH);
+	}
+	
+	function tearDown() {
+		unset($this->autoloader);
+	}
+	
+	function test_path_with_slash() {
+		$this->assertEquals(Autoloader::path_with_slash('dirname'), 'dirname/');
+		$this->assertEquals(Autoloader::path_with_slash('dirname/'), 'dirname/');
+	}
+	
+	/**
+	 * @depends test_path_with_slash
+	 */
+	function test_set_root_directory() {
+		$this->autoloader->set_root_directory('tests');
+		$this->assertEquals($this->autoloader->get_root_directory(), 'tests/');
+	}
+	
+	function test_classname_to_filename() {
+		$this->assertEquals(Autoloader::classname_to_filename('Foo'), 'foo');
+		$this->assertEquals(Autoloader::classname_to_filename('FooBar'), 'foo_bar');
+		$this->assertEquals(Autoloader::classname_to_filename('fooBar'), 'foo_bar');
+		$this->assertEquals(Autoloader::classname_to_filename('FooBarBaz'), 'foo_bar_baz');
+	}
+	
+	/**
+	 * @depends test_classname_to_filename
+	 */
+	function test_create_path() {
+		$this->assertEquals($this->autoloader->create_path('Foo'), PATH.'foo.php');
+		$this->assertEquals($this->autoloader->create_path('\Foo'), PATH.'foo.php');
+		$this->assertEquals($this->autoloader->create_path('Foo\Bar'), PATH.'foo/bar.php');
+		$this->assertEquals($this->autoloader->create_path('Foo\Bar\Baz'), PATH.'foo/bar/baz.php');
+		$this->assertEquals($this->autoloader->create_path('FooBar\Baz'), PATH.'foo_bar/baz.php');
+	}
+	
+	/**
+	 * @depends test_create_path
+	 * @expectedException Minimalistic\FileNotFoundError
+	 * @expectedExceptionMessage File "tests/_files/foobar.php" does not exist.
+	 */
+	function test_load_class_not_found() {
+		$this->autoloader->load_class('foobar');
+	}
+	
+	/**
+	 * @depends test_load_class_not_found
+	 */
+	function test_load_class() {
+		$this->assertTrue($this->autoloader->load_class('Foo'));
+		$this->assertTrue(class_exists('Foo', false));
+		$this->assertTrue($this->autoloader->load_class('Foo\Bar'));
+		$this->assertTrue(class_exists('Foo\Bar', false));
+	}
+	
+	/**
+	 * @depends test_load_class
+	 */
+	function test_register() {
+		$this->autoloader->register();
+		$this->assertTrue(class_exists('Baz'));
+	}
+	
+	function test_throw_errors() {
+		$this->assertTrue($this->autoloader->get_throw_errors());
+		$this->autoloader->set_throw_errors(false);
+		$this->assertFalse($this->autoloader->get_throw_errors());
+	}
+	
+	/**
+	 * @depends test_register
+	 * @depends test_throw_errors
+	 */
+	function test_register_prepend() {
+		$second_loader = new Autoloader(PATH.'second');
+		$this->autoloader->register();
+		$second_loader->register(true);  // Prepend so that the second loader attemps to load Bar first
+		$second_loader->set_throw_errors(false);
+		$this->assertInstanceOf('Foo', new FooBaz());
+	}
+}
+
+?>

+ 27 - 0
tests/test_base.php

@@ -0,0 +1,27 @@
+<?php
+
+require_once 'base.php';
+use Minimalistic\asprintf;
+
+class BaseExtension extends Minimalistic\Base {
+	function __construct($foo, $bar) {
+		$this->foo = $foo;
+		$this->bar = $bar;
+	}
+}
+
+class BaseTest extends PHPUnit_Framework_TestCase {
+	function test_create() {
+		$this->assertEquals(BaseExtension::create('a', 'b'), new BaseExtension('a', 'b'));
+	}
+	
+	function test_asprintf() {
+		$this->assertEquals(Minimalistic\asprintf('%(foo) baz', array('foo' => 'bar')), 'bar baz');
+		$this->assertEquals(Minimalistic\asprintf('%(foo) baz %(foo)',
+			array('foo' => 'bar')), 'bar baz bar');
+		$this->assertEquals(Minimalistic\asprintf('%(bar) baz %(foo)',
+			array('foo' => 'bar', 'bar' => 'foobar')), 'foobar baz bar');
+	}
+}
+
+?>

+ 113 - 0
tests/test_logger.php

@@ -0,0 +1,113 @@
+<?php
+
+require_once 'logger.php';
+use Minimalistic\Logger;
+
+define('NAME', 'Testlogger');
+define('FORMAT', '%(level): %(message)');
+
+class LoggerTest extends PHPUnit_Extensions_OutputTestCase {
+	function setUp() {
+		$this->logger = new Logger();
+		$this->logger->set_property('name', NAME);
+		$this->logger->set_format(FORMAT);
+	}
+	
+	function assert_dumps($expected) {
+		$this->assertEquals($this->logger->dumps(), $expected);
+	}
+	
+	function test_get_format() {
+		$this->assertEquals($this->logger->get_format(), FORMAT);
+	}
+	
+	function test_get_level() {
+		$this->assertEquals($this->logger->get_level(), Logger::WARNING);
+		$this->assertEquals($this->logger->get_level_name(), 'WARNING');
+	}
+	
+	/**
+	 * @depends test_get_level
+	 */
+	function test_set_level() {
+		$this->logger->set_level('info');
+		$this->assertEquals($this->logger->get_level(), Logger::INFO);
+		$this->logger->set_level('DEBUG');
+		$this->assertEquals($this->logger->get_level(), Logger::DEBUG);
+		$this->logger->set_level('WaRnInG');
+		$this->assertEquals($this->logger->get_level(), Logger::WARNING);
+		$this->logger->set_level(Logger::ERROR);
+		$this->assertEquals($this->logger->get_level(), Logger::ERROR);
+	}
+	
+	function test_format() {
+		$this->logger->error('test message');
+		$this->assert_dumps('ERROR: test message');
+	}
+	
+	function test_set_property() {
+		$this->logger->set_property('name', 'Logger');
+		$this->assertEquals($this->logger->get_formatted_property('name'), 'Logger');
+	}
+	
+	/**
+	 * @depends test_format
+	 */
+	function test_clear() {
+		$this->logger->warning('test message');
+		$this->logger->clear();
+		$this->assert_dumps('');
+	}
+	
+	/**
+	 * @depends test_set_level
+	 * @depends test_clear
+	 */
+	function test_process_level() {
+		$this->logger->info('test message');
+		$this->assert_dumps('');
+		$this->logger->warning('test message');
+		$this->assert_dumps('WARNING: test message');
+		$this->logger->critical('test message');
+		$this->assert_dumps("WARNING: test message\nCRITICAL: test message");
+		$this->logger->clear();
+		$this->logger->set_level('debug');
+		$this->logger->debug('test message');
+		$this->assert_dumps('DEBUG: test message');
+	}
+	
+	function test_get_formatted_property() {
+		$this->assertEquals($this->logger->get_formatted_property('name'), NAME);
+		$this->assertEquals($this->logger->get_formatted_property('loglevel'), 'WARNING');
+		$this->assertRegExp('/^\d{2}-\d{2}-\d{4}$/',
+			$this->logger->get_formatted_property('date'));
+		$this->assertRegExp('/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$/',
+			$this->logger->get_formatted_property('datetime'));
+		$this->assertRegExp('/^\d{2}:\d{2}:\d{2}$/',
+			$this->logger->get_formatted_property('time'));
+		$this->setExpectedException('\InvalidArgumentException');
+		$this->logger->get_formatted_property('foo');
+	}
+	
+	function test_dumps_property_format() {
+		$this->logger->warning('test message');
+		$this->logger->set_format('%(name): %(level): %(message)');
+		$this->assert_dumps(NAME.': WARNING: test message');
+	}
+	
+	/**
+	 * @depends test_process_level
+	 */
+	function test_dump() {
+		$this->logger->warning('test message');
+		$this->expectOutputString('WARNING: test message');
+		$this->logger->dump();
+	}
+	
+	function test_handle_exception() {
+		$this->logger->handle_exception(new Exception('test message'));
+		$this->assertNotEquals($this->logger->dumps(), '');
+	}
+}
+
+?>