Browse Source

Initial commit

main
Dustin Wilson 3 years ago
commit
ae4d0713d2
  1. 70
      .gitignore
  2. 4
      AUTHORS
  3. 22
      LICENSE
  4. 7
      README.md
  5. 136
      RoboFile.php
  6. 37
      composer.json
  7. 71
      composer.lock
  8. 68
      lib/Exception.php
  9. 72
      lib/MagicProperties.php
  10. 14
      robo
  11. 24
      tests/bootstrap.php
  12. 36
      tests/cases/TestException.php
  13. 122
      tests/cases/TestMagicProperties.php
  14. 23
      tests/phpunit.dist.xml
  15. 5
      vendor-bin/phpunit/composer.json
  16. 2111
      vendor-bin/phpunit/composer.lock
  17. 5
      vendor-bin/robo/composer.json
  18. 2007
      vendor-bin/robo/composer.lock

70
.gitignore

@ -0,0 +1,70 @@
# General
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
/vendor/
/vendor-bin/*/vendor
/tests/.phpunit.result.cache
/tests/coverage
cachegrind.out.*

4
AUTHORS

@ -0,0 +1,4 @@
Project leads
-------------
Dustin Wilson https://dustinwilson.com/
J. King https://jkingweb.ca/

22
LICENSE

@ -0,0 +1,22 @@
Copyright (c) 2021 Dustin Wilson, J. King
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.

7
README.md

@ -0,0 +1,7 @@
# Framework #
Common classes and traits used in many MensBeam projects
## Magic Properties ##
Let's face it. Getters and setters in PHP sucks. Instead of having getter and setter accessor methods for classes we instead have the `__get` and `__set` magic methods to handle all properties. Not only are they unwieldy to use when you have many properties they also become difficult to handle when inheriting, especially when traits are involved. This trait attempts to create hackish getter and setter methods that can be extended by simple inheritance.

136
RoboFile.php

@ -0,0 +1,136 @@
<?php
/** @license MIT
* Copyright 2017 , Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details */
use Robo\Result;
const BASE = __DIR__.\DIRECTORY_SEPARATOR;
const BASE_TEST = BASE."tests".\DIRECTORY_SEPARATOR;
define("IS_WIN", defined("PHP_WINDOWS_VERSION_MAJOR"));
define("IS_MAC", php_uname("s") === "Darwin");
error_reporting(0);
function norm(string $path): string {
$out = realpath($path);
if (!$out) {
$out = str_replace(["/", "\\"], \DIRECTORY_SEPARATOR, $path);
}
return $out;
}
class RoboFile extends \Robo\Tasks {
/** Runs the typical test suite
*
* Arguments passed to the task are passed on to PHPUnit. Thus one may, for
* example, run the following command and get the expected results:
*
* ./robo test --testsuite Tokenizer --exclude-group slow --testdox
*
* Please see the PHPUnit documentation for available options.
*/
public function test(array $args): Result {
return $this->runTests(escapeshellarg(\PHP_BINARY), "typical", $args);
}
/** Runs the full test suite
*
* This includes pedantic tests which may help to identify problems.
* See help for the "test" task for more details.
*/
public function testFull(array $args): Result {
return $this->runTests(escapeshellarg(\PHP_BINARY), "full", $args);
}
/**
* Runs a quick subset of the test suite
*
* See help for the "test" task for more details.
*/
public function testQuick(array $args): Result {
return $this->runTests(escapeshellarg(\PHP_BINARY), "quick", $args);
}
/** Produces a code coverage report
*
* By default this task produces an HTML-format coverage report in
* tests/coverage/. Additional reports may be produced by passing
* arguments to this task as one would to PHPUnit.
*/
public function coverage(array $args): Result {
// run tests with code coverage reporting enabled
$exec = $this->findCoverageEngine();
return $this->runTests($exec, "coverage", array_merge(["--coverage-html", BASE_TEST."coverage"], $args));
}
/** Produces a code coverage report, with redundant tests
*
* Depending on the environment, some tests that normally provide
* coverage may be skipped, while working alternatives are normally
* suppressed for reasons of time. This coverage report will try to
* run all tests which may cover code.
*
* See also help for the "coverage" task for more details.
*/
public function coverageFull(array $args): Result {
// run tests with code coverage reporting enabled
$exec = $this->findCoverageEngine();
return $this->runTests($exec, "typical", array_merge(["--coverage-html", BASE_TEST."coverage"], $args));
}
protected function findCoverageEngine(): string {
$dir = rtrim(ini_get("extension_dir"), "/").\DIRECTORY_SEPARATOR;
$ext = IS_WIN ? "dll" : (IS_MAC ? "dylib" : "so");
$php = escapeshellarg(\PHP_BINARY);
$code = escapeshellarg(BASE."lib");
if (extension_loaded("pcov")) {
return "$php -d pcov.enabled=1 -d pcov.directory=$code";
} elseif (extension_loaded("xdebug")) {
return "$php -d xdebug.mode=coverage";
} elseif (file_exists($dir."pcov.$ext")) {
return "$php -d extension=pcov.$ext -d pcov.enabled=1 -d pcov.directory=$code";
} elseif (file_exists($dir."xdebug.$ext")) {
return "$php -d zend_extension=xdebug.$ext -d xdebug.mode=coverage";
} else {
if (IS_WIN) {
$dbg = dirname(\PHP_BINARY)."\\phpdbg.exe";
$dbg = file_exists($dbg) ? $dbg : "";
} else {
$dbg = trim(`which phpdbg 2>/dev/null`);
}
if ($dbg) {
return escapeshellarg($dbg)." -qrr";
} else {
return $php;
}
}
}
protected function blackhole(bool $all = false): string {
$hole = IS_WIN ? "nul" : "/dev/null";
return $all ? ">$hole 2>&1" : "2>$hole";
}
protected function runTests(string $executor, string $set, array $args) : Result {
switch ($set) {
case "typical":
$set = ["--exclude-group", "optional"];
break;
case "quick":
$set = ["--exclude-group", "optional,slow"];
break;
case "coverage":
$set = ["--exclude-group", "optional,coverageOptional"];
break;
case "full":
$set = [];
break;
default:
throw new \Exception;
}
$execpath = norm(BASE."vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit");
$confpath = realpath(BASE_TEST."phpunit.dist.xml") ?: norm(BASE_TEST."phpunit.xml");
return $this->taskExec($executor)->option("-d", "zend.assertions=1")->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
}
}

37
composer.json

@ -0,0 +1,37 @@
{
"name": "mensbeam/framework",
"description": "Common classes and traits used in many Mensbeam projects",
"type": "library",
"require": {
"php": ">=7.4"
},
"scripts": {
"post-install-cmd": ["@composer bin all install"],
"post-update-cmd": ["@composer bin all update"]
},
"license": "MIT",
"authors": [
{
"name": "Dustin Wilson",
"email": "dustin@dustinwilson.com",
"homepage": "https://dustinwilson.com/"
}
],
"autoload": {
"psr-4": {
"MensBeam\\Framework\\": [
"lib/"
]
}
},
"autoload-dev": {
"psr-4": {
"MensBeam\\Framework\\TestCase\\": [
"tests/cases/"
]
}
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.3"
}
}

71
composer.lock

@ -0,0 +1,71 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c54c11d6e36c223b00cc483a01f990b2",
"packages": [],
"packages-dev": [
{
"name": "bamarni/composer-bin-plugin",
"version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/bamarni/composer-bin-plugin.git",
"reference": "9329fb0fbe29e0e1b2db8f4639a193e4f5406225"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/9329fb0fbe29e0e1b2db8f4639a193e4f5406225",
"reference": "9329fb0fbe29e0e1b2db8f4639a193e4f5406225",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0 || ^2.0",
"php": "^5.5.9 || ^7.0 || ^8.0"
},
"require-dev": {
"composer/composer": "^1.0 || ^2.0",
"symfony/console": "^2.5 || ^3.0 || ^4.0"
},
"type": "composer-plugin",
"extra": {
"class": "Bamarni\\Composer\\Bin\\Plugin"
},
"autoload": {
"psr-4": {
"Bamarni\\Composer\\Bin\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "No conflicts for your bin dependencies",
"keywords": [
"composer",
"conflict",
"dependency",
"executable",
"isolation",
"tool"
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
"source": "https://github.com/bamarni/composer-bin-plugin/tree/master"
},
"time": "2020-05-03T08:27:20+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.4"
},
"platform-dev": [],
"plugin-api-version": "2.1.0"
}

68
lib/Exception.php

@ -0,0 +1,68 @@
<?php
/**
* @license MIT
* Copyright 2021, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Framework;
class Exception extends \Exception {
const INVALID_CODE = 100;
const UNKNOWN_ERROR = 101;
const INCORRECT_PARAMETERS_FOR_MESSAGE = 102;
const UNREACHABLE_CODE = 103;
const NONEXISTENT_PROPERTY = 201;
const READONLY_PROPERTY = 202;
const ARGUMENT_TYPE_ERROR = 203;
protected static $messages = [
100 => 'Invalid error code',
101 => 'Unknown error; escaping',
102 => 'Incorrect number of parameters for Exception message; %s expected',
103 => 'Unreachable code',
201 => 'Property %s does not exist',
202 => 'Cannot write readonly property %s',
203 => 'Argument #%s ($%s) must be of type %s, %s given'
];
public function __construct(int $code, ...$args) {
if (!isset(self::$messages[$code])) {
throw new Exception(Exception::INVALID_CODE);
}
$message = self::$messages[$code];
$previous = null;
// @codeCoverageIgnoreStart
if ($args) {
// Grab a previous exception if there is one.
if ($args[0] instanceof \Throwable) {
$previous = array_shift($args);
} elseif (end($args) instanceof \Throwable) {
$previous = array_pop($args);
}
}
// @codeCoverageIgnoreEnd
// Count the number of replacements needed in the message.
preg_match_all('/(\%(?:\d+\$)?s)/', $message, $matches);
$count = count($matches[1]);
// If the number of replacements don't match the arguments then oops.
if (count($args) !== $count) {
throw new Exception(Exception::INCORRECT_PARAMETERS_FOR_MESSAGE, $count);
}
if ($count > 0) {
// Go through each of the arguments and run sprintf on the strings.
$message = call_user_func_array('sprintf', array_merge([$message], $args));
}
parent::__construct($message, $code, $previous);
}
}

72
lib/MagicProperties.php

@ -0,0 +1,72 @@
<?php
/**
* @license MIT
* Copyright 2021, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Framework;
trait MagicProperties {
public function __get(string $name) {
$methodName = $this->getMagicPropertyMethodName($name);
if ($methodName === null) {
throw new Exception(Exception::NONEXISTENT_PROPERTY, $name);
}
return call_user_func([ $this, $methodName ]);
}
public function __isset(string $name): bool {
return ($this->getMagicPropertyMethodName($name) !== null);
}
public function __set(string $name, $value) {
$methodName = $this->getMagicPropertyMethodName($name, false);
if ($methodName !== null) {
call_user_func([ $this, $methodName ], $value);
return;
}
if ($this->getMagicPropertyMethodName($name) !== null) {
throw new Exception(Exception::READONLY_PROPERTY, $name);
} else {
throw new Exception(Exception::NONEXISTENT_PROPERTY, $name);
}
}
public function __unset(string $name) {
$methodName = $this->getMagicPropertyMethodName($name, false);
if ($methodName === null) {
throw new Exception(Exception::READONLY_PROPERTY, $name);
}
call_user_func([ $this, $methodName ], null);
}
// Method_exists is case-insensitive because methods are case-insensitive in
// PHP. Properties in PHP 8 are sensitive, so let's use reflection to check
// against the actual name to get a case sensitive match like methods should be!
private function getMagicPropertyMethodName(string $name, bool $get = true): ?string {
static $protectedMethodsList = null;
$methodName = "__" . (($get) ? 'get' : 'set') . "_{$name}";
if (method_exists($this, $methodName)) {
if ($protectedMethodsList === null) {
$reflector = new \ReflectionClass($this);
// Magic property methods are protected
$protectedMethodsList = $reflector->getMethods(\ReflectionMethod::IS_PROTECTED);
}
foreach ($protectedMethodsList as $method) {
if ($method->name === $methodName) {
return $methodName;
}
}
}
return null;
}
}

14
robo

@ -0,0 +1,14 @@
#! /bin/sh
base=`dirname "$0"`
roboCommand="$1"
if [ $# -eq 0 ]; then
"$base/vendor/bin/robo"
else
shift
ulimit -n 2048
if [ "$1" = "clean" ]; then
"$base/vendor/bin/robo" "$roboCommand" "$@"
else
"$base/vendor/bin/robo" "$roboCommand" -- "$@"
fi
fi

24
tests/bootstrap.php

@ -0,0 +1,24 @@
<?php
/** @license MIT
* Copyright 2017 , Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace MensBeam\Framework;
const NS_BASE = __NAMESPACE__."\\";
define(NS_BASE."BASE", dirname(__DIR__).DIRECTORY_SEPARATOR);
const DOCROOT = BASE."tests".DIRECTORY_SEPARATOR."docroot".DIRECTORY_SEPARATOR;
ini_set("memory_limit", "-1");
ini_set("zend.assertions", "1");
ini_set("assert.exception", "true");
error_reporting(\E_ALL);
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
if (function_exists("xdebug_set_filter")) {
if (defined("XDEBUG_PATH_INCLUDE")) {
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_INCLUDE, [BASE."lib/"]);
} else {
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_WHITELIST, [BASE."lib/"]);
}
}

36
tests/cases/TestException.php

@ -0,0 +1,36 @@
<?php
/**
* @license MIT
* Copyright 2021, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Framework\TestCase;
use MensBeam\Framework\Exception;
/** @covers \MensBeam\Framework\Exception */
class TestException extends \PHPUnit\Framework\TestCase {
public function provideConstructorFailures(): iterable {
return [
[ function() {
$d = new Exception(2112);
}, Exception::INVALID_CODE ],
[ function() {
throw new Exception(Exception::UNKNOWN_ERROR, 'FAIL');
}, Exception::INCORRECT_PARAMETERS_FOR_MESSAGE ]
];
}
/**
* @dataProvider provideConstructorFailures
* @covers \MensBeam\Framework\Exception::__construct
*/
public function testConstructorFailures(\Closure $closure, int $errorCode): void {
$this->expectException(Exception::class);
$this->expectExceptionCode($errorCode);
$closure();
}
}

122
tests/cases/TestMagicProperties.php

@ -0,0 +1,122 @@
<?php
/**
* @license MIT
* Copyright 2021, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\Framework\{
Exception,
MagicProperties
};
/** @covers \MensBeam\Framework\MagicProperties */
class TestMagicProperties extends \PHPUnit\Framework\TestCase {
public function provideFailures(): iterable {
$ook = new class {
use MagicProperties;
protected ?string $_eek = 'eek';
protected ?string $_ook = 'ook';
protected function __get_eek(): ?string {
return $this->_eek;
}
protected function __get_ook(): ?string {
return $this->_ook;
}
protected function __set_ook(?string $value): void {
$this->_ook = $value;
}
};
return [
[ function() use($ook) {
$ook->ack;
}, Exception::NONEXISTENT_PROPERTY ],
[ function() use($ook) {
$ook->ack = 'ack';
}, Exception::NONEXISTENT_PROPERTY ],
[ function() use($ook) {
$ook->eek = 'ook';
}, Exception::READONLY_PROPERTY ],
[ function() use($ook) {
unset($ook->eek);
}, Exception::READONLY_PROPERTY ]
];
}
/**
* @dataProvider provideFailures
* @covers \MensBeam\Framework\MagicProperties::__get
* @covers \MensBeam\Framework\MagicProperties::__set
* @covers \MensBeam\Framework\MagicProperties::__unset
*/
public function testFailures(\Closure $closure, int $errorCode): void {
$this->expectException(Exception::class);
$this->expectExceptionCode($errorCode);
$closure();
}
/** @covers \MensBeam\Framework\MagicProperties::__isset */
public function testIsset(): void {
$ook = new class {
use MagicProperties;
protected function __get_ook(): ?string {
return 'ook';
}
};
$this->assertTrue(isset($ook->ook));
}
/** @covers \MensBeam\Framework\MagicProperties::__unset */
public function testUnset(): void {
$ook = new class {
use MagicProperties;
protected ?string $_ook = 'ook';
protected function __get_ook(): ?string {
return $this->_ook;
}
protected function __set_ook(?string $value): void {
$this->_ook = $value;
}
};
unset($ook->ook);
$this->assertNull($ook->ook);
}
/** @covers \MensBeam\Framework\MagicProperties::__set */
public function testSet(): void {
$ook = new class {
use MagicProperties;
protected ?string $_ook = 'ook';
protected function __get_ook(): ?string {
return $this->_ook;
}
protected function __set_ook(?string $value): void {
$this->_ook = $value;
}
};
$ook->ook = 'eek';
$this->assertSame('eek', $ook->ook);
}
}

23
tests/phpunit.dist.xml

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
colors="true"
bootstrap="bootstrap.php"
convertErrorsToExceptions="false"
convertNoticesToExceptions="false"
convertWarningsToExceptions="false"
beStrictAboutTestsThatDoNotTestAnything="true"
forceCoversAnnotation="true">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">../lib</directory>
</include>
</coverage>
<testsuites>
<testsuite name="GetSet">
<file>cases/TestException.php</file>
<file>cases/TestMagicProperties.php</file>
</testsuite>
</testsuites>
</phpunit>

5
vendor-bin/phpunit/composer.json

@ -0,0 +1,5 @@
{
"require": {
"phpunit/phpunit": "^8.5 | ^9.0"
}
}

2111
vendor-bin/phpunit/composer.lock

File diff suppressed because it is too large

5
vendor-bin/robo/composer.json

@ -0,0 +1,5 @@
{
"require": {
"consolidation/robo": "^2.0"
}
}

2007
vendor-bin/robo/composer.lock

File diff suppressed because it is too large
Loading…
Cancel
Save