Commit 34905f8b authored by Taddeus Kroes's avatar Taddeus Kroes

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.
parents
*.swp
*.~
build/*
-------
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.
\ No newline at end of file
<?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);
}
}
?>
\ No newline at end of file
<?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
);
}
?>
\ No newline at end of file
<?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));
}
}
?>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?php
class Baz {}
?>
\ No newline at end of file
<?php
class Foo {}
?>
\ No newline at end of file
<?php
namespace Foo;
class Bar {}
?>
\ No newline at end of file
<?php
class FooBaz extends Foo {}
?>
\ No newline at end of file
<?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());
}
}
?>
\ No newline at end of file
<?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');
}
}
?>
\ No newline at end of file
<?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(), '');
}
}
?>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment