Merge branch '122-plugin-system' into 'development'

Resolve "Plugin system"

Resolves multiple bugs and implements the plugin system.

> Closes #122

See merge request fuzeworks/core!61
This commit is contained in:
Abel Hoogeveen 2018-02-21 23:04:06 +01:00
commit 38842ea052
41 changed files with 1588 additions and 42 deletions

View File

@ -0,0 +1,38 @@
<?php
/**
* FuzeWorks Application Skeleton.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.4
*
* @version Version 1.1.4
*/
/**
* Special settings for plugins. Allows the administrator to disable plugins. Can be edited manually or automatically.
*/
return array(
'disabled_plugins' => array(),
);

View File

@ -20,15 +20,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2016, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.1
*
* @version Version 1.1.1
* @version Version 1.1.4
*/
/**
* Possible values:
* Default callable: Adds a route that changes the URL structure. Sends all matches to the defaultCallable router
* 'routingString'
*
* Custom callable: Adds a route that sends all matches to the provided callable. Allows user to replace defaultCallable
* 'routingString' => array('callable' => array(CALLABLE))
*
* Dynamic rewrite: Adds a route that rewrites an URL to a specific controller and method configuration, using a callable. The callable can dynamically determine which page to load.
* 'routingString' => CALLABLE
*
* Static rewrite: Adds a route that rewrites and URL to a specific controller and method using a fixed route. This allows for pre-determined rewrites of pages.
* 'routingString' => 'standard/index'
*
* Example routingString: '/^(?P<controller>.*?)(|\/(?P<function>.*?)(|\/(?P<parameters>.*?)))$/'
*/
return array(
);

View File

@ -63,6 +63,7 @@ return array(
'function_trigger' => 'm',
'directory_trigger' => 'd',
/*
|--------------------------------------------------------------------------
| Allowed URL Characters
@ -85,6 +86,16 @@ return array(
*/
'permitted_uri_chars' => 'a-z 0-9~%.:_\-',
/*
|--------------------------------------------------------------------------
| Translate URI Dashes
|--------------------------------------------------------------------------
| Determines whether dashes in controller & method segments
| should be automatically replaced by underscores.
|
*/
'translate_uri_dashes' => FALSE,
/*
|--------------------------------------------------------------------------
| URI PROTOCOL

View File

@ -30,9 +30,7 @@
* @version Version 1.0.0
*/
use FuzeWorks\Logger;
use FuzeWorks\Config;
use FuzeWorks\Exception\ConfigException;
use FuzeWorks\Factory;
use FuzeWorks\Database;
use FuzeWorks\Exception\DatabaseException;
use FuzeWorks\Core;

View File

@ -104,6 +104,14 @@ class Config
return $this->getConfig($configName);
}
/**
* Clears all the config files and discards all changes not committed
*/
public function discardConfigFiles()
{
$this->cfg = array();
}
/**
* Determine whether the file exists and, if so, load the ConfigORM
*

View File

@ -70,6 +70,13 @@ class Core
public static $logDir;
/**
* The HTTP status code of the current request
*
* @var int $http_status_code Status code
*/
public static $http_status_code = 200;
/**
* Initializes the core.
*
@ -111,15 +118,18 @@ class Core
Events::disable();
}
// And fire the coreStartEvent
$event = Events::fireEvent('coreStartEvent');
if ($event->isCancelled()) {
return true;
}
// And initialize multiple classes
$container->layout->init();
Language::init();
// And load all the plugins
$container->plugins->loadHeaders();
// And fire the coreStartEvent
$event = Events::fireEvent('coreStartEvent');
if ($event->isCancelled()) {
exit;
}
}
/**

View File

@ -0,0 +1,93 @@
<?php
/**
* FuzeWorks.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2016, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.4
*
* @version Version 1.1.4
*/
namespace FuzeWorks\Event;
use FuzeWorks\Event;
/**
* Event that will get fired when a plugin is retrieved.
*
* @author Abel Hoogeveen <abel@techfuze.net>
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
*/
class PluginGetEvent extends Event
{
/**
* The name of the plugin that should be loaded
*
* @var string
*/
public $pluginName;
/**
* Array of directories where to look at for the plugin
*
* @var array
*/
public $directories = array();
/**
* Potential plugin to return instead. If set, the plugins class will return this object
*
* @var object
*/
public $plugin = null;
public function init($pluginName, array $directories = array())
{
$this->pluginName = $pluginName;
$this->directories = $directories;
}
/**
* Allows listeners to set a plugin that will be returned instead.
*
* @param object $plugin
*/
public function setPlugin($plugin)
{
$this->plugin = $plugin;
}
/**
* Plugin that will be returned if set by a listener.
*
* @return object|null $plugin
*/
public function getPlugin()
{
return $this->plugin;
}
}
?>

View File

@ -0,0 +1,45 @@
<?php
/**
* FuzeWorks.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.4
*
* @version Version 1.1.4
*/
namespace FuzeWorks\Exception;
/**
* Class PluginException.
*
* @author Abel Hoogeveen <abel@techfuze.net>
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
*/
class PluginException extends Exception
{
}
?>

View File

@ -161,6 +161,12 @@ class Factory
*/
public $router;
/**
* Plugins Object
* @var Plugins
*/
public $plugins;
/**
* Factory instance constructor. Should only really be called once
* @return void
@ -187,6 +193,7 @@ class Factory
$this->security = new Security();
$this->input = new Input();
$this->router = new Router();
$this->plugins = new Plugins();
return true;
}

View File

@ -524,6 +524,9 @@ class Logger {
self::log('Sending header HTTP/1.1 ' . $errno . ' ' . $http_codes[$errno]);
header('HTTP/1.1 ' . $errno . ' ' . $http_codes[$errno]);
// Set the status code
Core::$http_status_code = $errno;
// Do we want the error-layout with it?
if ($layout == false) {
return false;

View File

@ -0,0 +1,39 @@
<?php
/**
* FuzeWorks.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.4
*
* @version Version 1.1.4
*/
namespace FuzeWorks;
interface PluginInterface
{
public function init();
}

288
src/FuzeWorks/Plugins.php Normal file
View File

@ -0,0 +1,288 @@
<?php
/**
* FuzeWorks.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.4
*
* @version Version 1.1.4
*/
namespace FuzeWorks;
use FuzeWorks\Exception\PluginException;
/**
* Plugins Class.
*
* Plugins are small component that can be implemented into FuzeWorks and will run upon its start. When FuzeWorks loads, a header file of every plugin will be loaded. This allows plugins to hook into routes, events, factory components and other parts of the framework. This for instance allows the creation of an administration panel, or a contnet management system.
*
* To make a plugin there are 2 requirements:
* 1. A plugin class file
* 2. A plugin header file
*
* To create the plugin, create a directory (with the name of the plugin) in the Plugin folder, inside the Application environment. Next you should add a header.php file in this directory.
* This file needs to be in the FuzeWorks\Plugins namespace, and be named *PluginName*Header. For example: TestHeader.
* It is recommended that this header file implements the FuzeWorks\PluginInterface. All headers must have the init() method. This method will run upon starting FuzeWorks.
*
* Next a plugin class should be created. This file should be named the same as the folder, and be in the Application\Plugin namespace. An alternative classname can be set in the header, by creating a public $className variable. This plugin can be called using the $plugins->get() method.
*
* @todo Implement events
* @author Abel Hoogeveen <abel@techfuze.net>
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
*/
class Plugins
{
/**
* Array of all the paths where plugins can be found
*
* @var array Plugin paths
*/
protected $pluginPaths = array();
/**
* Array of loaded Plugins, so that they won't be reloaded
* Plugins only end up here after being explicitly loaded. Header files do NOT count.
*
* @var array Array of loaded plugins
*/
protected $plugins = array();
/**
* Array of plugin header classes.
* Loaded upon startup. Provide details on what class should load for the plugin.
*
* @var array Array of loaded plugin header classes
*/
protected $headers = array();
/**
* Config file for the plugin system
*
* @var ConfigORM
*/
protected $cfg;
/**
* Called upon creation of the plugins class.
*
* @param string $directory The directory
* @return void
*/
public function __construct()
{
$this->pluginPaths[] = Core::$appDir . DS . 'Plugins';
$this->cfg = Factory::getInstance()->config->plugins;
}
/**
* Load the header files of all plugins.
*/
public function loadHeaders()
{
// Cycle through all pluginPaths
$this->headers = array();
foreach ($this->pluginPaths as $pluginPath) {
// If directory does not exist, skip it
if (!file_exists($pluginPath) || !is_dir($pluginPath))
{
continue;
}
// Fetch the contents of the path
$pluginPathContents = array_diff(scandir($pluginPath), array('..', '.'));
// Now go through each entry in the plugin folder
foreach ($pluginPathContents as $pluginFolder) {
if (!is_dir($pluginPath . DS . $pluginFolder))
{
continue;
}
// If a header file exists, use it
$file = $pluginPath . DS . $pluginFolder . DS . 'header.php';
$pluginName = ucfirst($pluginFolder);
$className = '\FuzeWorks\Plugins\\'.$pluginName.'Header';
if (file_exists($file))
{
// And load it
if (in_array($pluginName, $this->cfg->disabled_plugins))
{
$this->headers[$pluginName] = 'disabled';
}
else
{
require_once($file);
$header = new $className();
$this->headers[$pluginName] = $header;
$this->headers[$pluginName]->init();
Factory::getInstance()->logger->log('Loaded Plugin Header: \'' . $pluginName . '\'');
}
}
// If it doesn't exist, skip it
continue;
}
}
}
/**
* Get a plugin.
*
* @param string $pluginName Name of the plugin
* @param array $parameters Parameters to send to the __construct() method
* @param array $directory Directory to search for plugins in
* @return object Plugin
*/
public function get($pluginName, array $parameters = null, array $directory = null)
{
if (empty($pluginName))
{
throw new PluginException("Could not load plugin. No name provided", 1);
}
// First get the directories where the plugin can be located
$directories = (is_null($directory) ? $this->pluginPaths : $directory);
// Determine the name of the plugin
$pluginFolder = $pluginName;
$pluginName = ucfirst($pluginName);
// Fire pluginGetEvent, and cancel or return custom plugin if required
$event = Events::fireEvent('pluginGetEvent', $pluginName, $directories);
if ($event->isCancelled())
{
return false;
}
elseif ($event->getPlugin() != null)
{
return $event->getPlugin();
}
// Otherwise just set the variables
$pluginName = $event->pluginName;
$directories = $event->directories;
// Check if the plugin is already loaded and return directly
if (isset($this->plugins[$pluginName]))
{
return $this->plugins[$pluginName];
}
// Check if the plugin header exists
if (!isset($this->headers[$pluginName]))
{
throw new PluginException("Could not load plugin. Plugin header does not exist", 1);
}
// If disabled, don't bother
if (in_array($pluginName, $this->cfg->disabled_plugins))
{
throw new PluginException("Could not load plugin. Plugin is disabled", 1);
}
// Determine what file to load
$header = $this->headers[$pluginName];
if (method_exists($header, 'getPlugin'))
{
$this->plugins[$pluginName] = $header->getPlugin();
Factory::getInstance()->logger->log('Loaded Plugin: \'' . $pluginName . '\'');
return $this->plugins[$pluginName];
}
$classFile = (isset($header->classFile) ? $header->classFile : $pluginName.".php");
$className = (isset($header->className) ? $header->className : '\Application\Plugin\\'.$pluginName);
// Find the correct file
$pluginFile = '';
foreach ($directories as $pluginPath) {
$file = $pluginPath . DS . $pluginFolder . DS . $classFile;
if (file_exists($file))
{
$pluginFile = $file;
break;
}
}
// If not found, throw exception
if (empty($pluginFile))
{
throw new PluginException("Could not load plugin. Class file does not exist", 1);
}
// Attempt to load the plugin
require_once($pluginFile);
if (!class_exists($className, false))
{
throw new PluginException("Could not load plugin. Class does not exist", 1);
}
$this->plugins[$pluginName] = new $className($parameters);
Factory::getInstance()->logger->log('Loaded Plugin: \'' . $pluginName . '\'');
// And return it
return $this->plugins[$pluginName];
}
/**
* Add a path where plugins can be found
*
* @param string $directory The directory
* @return void
*/
public function addPluginPath($directory)
{
if (!in_array($directory, $this->pluginPaths))
{
$this->pluginPaths[] = $directory;
}
}
/**
* Remove a path where plugins can be found
*
* @param string $directory The directory
* @return void
*/
public function removePluginPath($directory)
{
if (($key = array_search($directory, $this->pluginPaths)) !== false)
{
unset($this->pluginPaths[$key]);
}
}
/**
* Get a list of all current pluginPaths
*
* @return array Array of paths where plugins can be found
*/
public function getPluginPaths(): array
{
return $this->pluginPaths;
}
}

View File

@ -82,7 +82,6 @@ namespace FuzeWorks;
* @copyright Copyright (c) 2013 - 2016, Techfuze. (http://techfuze.net)
*
* @todo Implement Query Strings
* @todo Implement Unit tests
*/
class Router
{
@ -102,14 +101,11 @@ class Router
protected $matches = null;
/**
* Translate URI dashes
* Directory from which to load the controllers
*
* Determines whether dashes in controller & method segments
* should be automatically replaced by underscores.
*
* @var bool
* @var string Controller directory
*/
protected $translate_uri_dashes = false;
protected $controller_directory;
/**
* The Config class used to get configurations
@ -162,6 +158,7 @@ class Router
$this->logger = $factory->logger;
$this->events = $factory->events;
$this->output = $factory->output;
$this->controller_directory = Core::$appDir . DS . 'Controller';
// Start parsing the routing
$this->parseRouting();
@ -188,12 +185,17 @@ class Router
foreach ($routes as $route => $value)
{
// Check if the route format is using HTTP verbs
if (is_array($value))
if (is_int($route))
{
$route = $value;
$value = array($this, 'defaultCallable');
}
elseif (is_array($value))
{
$value = array_change_key_case($value, CASE_LOWER);
if (isset($value[$http_verb]))
{
$value = $value['http_verb'];
$value = $value[$http_verb];
}
else
{
@ -208,6 +210,26 @@ class Router
}
}
/**
* Returns the directory from which all controllers shall be loaded
*
* @return string
*/
public function getControllerDirectory(): string
{
return $this->controller_directory;
}
/**
* Sets the directory from which all controllers shall be loaded
*
* @param string Controller directory
*/
public function setControllerDirectory($directory)
{
$this->controller_directory = $directory;
}
/**
* Returns an array with all the routes.
*
@ -331,15 +353,15 @@ class Router
}
// If the callable is satisfied, break away
if (!$performLoading || !$this->loadCallable($matches, $route))
if (!$performLoading || $this->loadCallable($matches, $route))
{
return false;
return true;
}
// Otherwise try other routes
continue;
}
elseif ( ! is_string($value) && is_callable($value))
elseif ( is_callable($value) )
{
// Prepare the callable
array_shift($matches);
@ -351,9 +373,14 @@ class Router
{
$value = preg_replace('#^'.$route.'$#', $value, $event->path);
}
if ($performLoading === true)
{
// Now run the defaultRouter for when something is not a callable
$this->routeDefault(explode('/', $value), $route);
return true;
}
// Now run the defaultRouter for when something is not a callable
$this->routeDefault(explode('/', $value), $route);
return false;
}
}
@ -364,8 +391,10 @@ class Router
if ($performLoading === true)
{
$this->routeDefault(array_values($this->uri->segments), '.*$');
return false;
return true;
}
return false;
}
/**
@ -384,7 +413,7 @@ class Router
$segments[0] = $this->config->routing->default_controller;
}
if ($this->translate_uri_dashes === true)
if ($this->config->routing->translate_uri_dashes === true)
{
$segments[0] = str_replace('-', '_', $segments[0]);
if (isset($segments[1]))
@ -443,12 +472,12 @@ class Router
$args['route'] = $event->route;
if (!is_callable($event->callable)) {
if (isset($this->callable['controller'])) {
if (isset($event->callable['controller'])) {
// Reset the arguments and fetch from custom callable
$args = array();
$args['controller'] = isset($this->callable['controller']) ? $this->callable['controller'] : (isset($matches['controller']) ? $matches['controller'] : null);
$args['function'] = isset($this->callable['function']) ? $this->callable['function'] : (isset($matches['function']) ? $matches['function'] : null);
$args['parameters'] = isset($this->callable['parameters']) ? $this->callable['parameters'] : (isset($matches['parameters']) ? explode('/', $matches['parameters']) : null);
$args['controller'] = isset($event->callable['controller']) ? $event->callable['controller'] : (isset($matches['controller']) ? $matches['controller'] : null);
$args['function'] = isset($event->callable['function']) ? $event->callable['function'] : (isset($matches['function']) ? $matches['function'] : null);
$args['parameters'] = isset($event->callable['parameters']) ? $event->callable['parameters'] : (isset($matches['parameters']) ? explode('/', $matches['parameters']) : null);
$this->callable = array('\FuzeWorks\Router', 'defaultCallable');
} else {
@ -469,9 +498,9 @@ class Router
}
$this->logger->stopLevel();
$skip = call_user_func_array($this->callable, array($args)) === false;
$skip = call_user_func_array($this->callable, array($args)) === true;
if ($skip) {
if (!$skip) {
$this->logger->log('Callable not satisfied, skipping to next callable');
}
@ -486,7 +515,7 @@ class Router
* This callable will do the 'old skool' routing. It will load the controllers from the controller-directory
* in the application-directory.
*/
public function defaultCallable($arguments = array())
public function defaultCallable($arguments = array()): bool
{
$this->logger->log('Default callable called!');
@ -496,8 +525,8 @@ class Router
// Construct file paths and classes
$class = '\Application\Controller\\'.ucfirst($controller);
$directory = Core::$appDir . DS . 'Controller';
$file = $directory . DS .'controller.'.$controller.'.php';
$directory = $this->controller_directory;
$file = $directory . DS . strtolower('controller.'.$controller.'.php');
$event = Events::fireEvent('routerLoadControllerEvent',
$file,
@ -510,14 +539,14 @@ class Router
// Cancel if requested to do so
if ($event->isCancelled()) {
return;
return false;
}
// Check if the file exists
if (file_exists($event->file)) {
if (!class_exists($event->className)) {
$this->logger->log('Loading controller '.$event->className.' from file: '.$event->file);
include $event->file;
require $event->file;
}
// Get the path the controller should know about
@ -528,7 +557,7 @@ class Router
// If the controller does not want a function to be loaded, provide a halt parameter.
if (isset($this->callable->halt)) {
return;
return false;
}
// Check if method exists or if there is a caller function
@ -537,11 +566,12 @@ class Router
$methodEvent = Events::fireEvent('routerCallMethodEvent');
if ($methodEvent->isCancelled())
{
return;
return false;
}
// Execute the function on the controller
$this->output->append_output($this->callable->{$event->function}($event->parameters));
return true;
} else {
// Function could not be found
$this->logger->log('Could not find function '.$event->function.' on controller '.$event->className);
@ -552,5 +582,7 @@ class Router
$this->logger->log('Could not find controller '.$event->className);
$this->logger->http_error(404);
}
return false;
}
}

View File

@ -54,7 +54,6 @@ class Standard extends ControllerAbstract
*/
public function index($path = null)
{
$this->output->cache(60);
$this->layout->view('home');
$this->layout->display('home');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Controller;
class Standard
{
public function index(){}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Application\Controller;
class Test_Route_Default_Uri_Dashes_Succeed
{
public function index(){}
public function dashed_method() {}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Controller;
class TestDefaultCallable
{
public function index(){}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Application\Controller;
class TestDefaultCallableHalt
{
public $halt = true;
public function index() {}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Controller;
class TestDefaultCallableMissingMethod
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Controller;
class TestLoadCallableToDefaultCallable
{
public function index() {}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Controller;
class TestLoadController
{
public function index(){}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Application\Controller;
class TestMatchingRoute
{
public function index(){}
public function methodInFirstArgument(){}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Controller;
class TestRouteDefault
{
public function index(){}
}

View File

@ -33,6 +33,7 @@ use PHPUnit\Framework\TestCase;
use FuzeWorks\Events;
use FuzeWorks\Layout;
use FuzeWorks\Factory;
use FuzeWorks\Core;
use FuzeWorks\LoggerTracyBridge;
/**
@ -58,10 +59,16 @@ abstract class CoreTestAbstract extends TestCase
// Reset the layout manager
Factory::getInstance()->layout->reset();
// Reset all config files
Factory::getInstance()->config->discardConfigFiles();
// Re-enable events, in case they have been disabled
Events::enable();
// Clear the output
Factory::getInstance()->output->set_output('');
// Reset the HTTP status code
Core::$http_status_code = 200;
}
}

View File

@ -0,0 +1,176 @@
<?php
/**
* FuzeWorks.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.4
*
* @version Version 1.1.4
*/
use FuzeWorks\Factory;
use FuzeWorks\Plugins;
/**
* Class PluginsTest.
*
* Plugins testing suite, will test basic loading of and management of Plugins
*/
class pluginTest extends CoreTestAbstract
{
protected $plugins;
public function setUp()
{
$this->plugins = new Plugins();
$this->plugins->addPluginPath('tests'.DS.'plugins');
$this->plugins->loadHeaders();
}
public function testGetPluginsClass()
{
$this->assertInstanceOf('FuzeWorks\Plugins', $this->plugins);
}
/**
* @depends testGetPluginsClass
*/
public function testLoadPlugin()
{
$this->assertInstanceOf('\Application\Plugin\TestLoadPlugin', $this->plugins->get('testLoadPlugin'));
}
/**
* @depends testLoadPlugin
*/
public function testReloadPlugin()
{
$this->assertSame($this->plugins->get('testReloadPlugin'), $this->plugins->get('testReloadPlugin'));
}
/**
* @depends testLoadPlugin
* @expectedException FuzeWorks\Exception\PluginException
*/
public function testMissingHeader()
{
$this->plugins->get('testMissingHeader');
}
/**
* @depends testLoadPlugin
*/
public function testGetPluginMethod()
{
$this->assertEquals('test_string', $this->plugins->get('testGetPluginMethod'));
}
/**
* @depends testLoadPlugin
* @expectedException FuzeWorks\Exception\PluginException
*/
public function testMissingPlugin()
{
$this->plugins->get('testMissingPlugin');
}
/**
* @depends testLoadPlugin
* @expectedException FuzeWorks\Exception\PluginException
*/
public function testInvalidClass()
{
$this->plugins->get('testInvalidClass');
}
/**
* @expectedException FuzeWorks\Exception\PluginException
*/
public function testGetMissingName()
{
$this->plugins->get('');
}
/**
* @depends testLoadPlugin
* @expectedException FuzeWorks\Exception\PluginException
*/
public function testDisabledPlugin()
{
Factory::getInstance()->config->plugins->disabled_plugins = array('TestDisabledPlugin');
$this->plugins->loadHeaders();
$this->plugins->get('testDisabledPlugin');
}
/**
* @depends testLoadPlugin
* @expectedException FuzeWorks\Exception\PluginException
*/
public function testRunInvalidDirectory()
{
$this->plugins->addPluginPath('exists_not');
$this->plugins->loadHeaders();
$this->plugins->get('testRunInvalidDirectory');
}
public function testAddPluginPath()
{
// Add the pluginPath
$this->plugins->addPluginPath('tests'.DS.'plugins'.DS.'testAddPluginPath');
// And try to load it again
$this->plugins->loadHeaders();
$this->assertInstanceOf('Application\Plugin\ActualPlugin', $this->plugins->get('ActualPlugin'));
}
/**
* @depends testAddPluginPath
*/
public function testRemovePluginPath()
{
// Test if the path does NOT exist
$this->assertFalse(in_array('tests'.DS.'plugins'.DS.'testRemovePluginPath', $this->plugins->getPluginPaths()));
// Add it
$this->plugins->addPluginPath('tests'.DS.'plugins'.DS.'testRemovePluginPath');
// Assert if it's there
$this->assertTrue(in_array('tests'.DS.'plugins'.DS.'testRemovePluginPath', $this->plugins->getPluginPaths()));
// Remove it
$this->plugins->removePluginPath('tests'.DS.'plugins'.DS.'testRemovePluginPath');
// And test if it's gone again
$this->assertFalse(in_array('tests'.DS.'plugins'.DS.'testRemovePluginPath', $this->plugins->getPluginPaths()));
}
public function tearDown()
{
$factory = Factory::getInstance();
$factory->config->plugins->revert();
}
}

View File

@ -0,0 +1,500 @@
<?php
/**
* FuzeWorks.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2016, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.2
*
* @version Version 1.1.2
*/
use FuzeWorks\Router;
use FuzeWorks\Factory;
use FuzeWorks\Core;
use FuzeWorks\Logger;
/**
* Class RouterTest.
*
* Core testing suite, will test basic core functionality of the FuzeWorks Router
*/
class routerTest extends CoreTestAbstract
{
protected $router;
public function setUp()
{
$this->router = new Router();
$this->router->setControllerDirectory('tests'.DS.'controller');
}
public function testGetRouterClass()
{
$this->assertInstanceOf('FuzeWorks\Router', $this->router);
}
public function testGetSetRouterDirectory()
{
$directory = 'tests' . DS . 'controller';
$this->router->setControllerDirectory($directory);
$this->assertEquals($directory, $this->router->getControllerDirectory($directory));
}
/* Route Parsing ------------------------------------------------------ */
/**
* @depends testGetRouterClass
*/
public function testAddRoutes()
{
$this->router->addRoute('testRoute', function(){});
$this->assertArrayHasKey('testRoute', $this->router->getRoutes());
}
/**
* @depends testAddRoutes
*/
public function testAppendRoutes()
{
$testRouteFunction = array(function(){});
$testAppendRouteFunction = array(function(){});
$this->router->addRoute('testRoute', $testRouteFunction);
$this->router->addRoute('testAppendRoute', $testAppendRouteFunction, false);
// Test if the order is correct
$this->assertSame(array('testRoute' => $testRouteFunction, 'testAppendRoute' => $testAppendRouteFunction), $this->router->getRoutes());
// Test if the order is not incorrect
$this->assertNotSame(array('testAppendRoute' => $testAppendRouteFunction,'testRoute' => $testRouteFunction), $this->router->getRoutes());
}
/**
* @depends testAddRoutes
*/
public function testRemoveRoutes()
{
// First add
$this->router->addRoute('testRemoveRoute', function(){});
$this->assertArrayHasKey('testRemoveRoute', $this->router->getRoutes());
// Then remove
$this->router->removeRoute('testRemoveRoute');
$this->assertArrayNotHasKey('testRemoveRoute', $this->router->getRoutes());
}
/**
* @depends testAddRoutes
*/
public function testParseRouting()
{
// Prepare the routes so they can be parsed
$config = Factory::getInstance()->config;
$config->routes->{'testParseRouting'} = function(){};
$this->router = new Router();
// Now verify whether the passing has gone correctly
$this->assertArrayHasKey('testParseRouting', $this->router->getRoutes());
}
/**
* @depends testParseRouting
*/
public function testVerbParsing()
{
// Prepare the routes so they can be parsed
$config = Factory::getInstance()->config;
$getFunction = function($get){};
$postFunction = function($post){};
$config->routes->{'testVerbPassing'} = array('GET' => $getFunction, 'POST' => $postFunction);
$this->router = new Router();
// Now verify whether the passing has gone correctly
$routes = $this->router->getRoutes();
$this->assertArrayHasKey('testVerbPassing', $routes);
$this->assertSame($getFunction, $routes['testVerbPassing']);
$this->assertNotSame($postFunction, $routes['testVerbPassing']);
}
/**
* @depends testVerbParsing
*/
public function testInvalidParsing()
{
// Prepare the routes so they can be parsed
$config = Factory::getInstance()->config;
$config->routes->{'testInvalidParsing'} = array('NOTGET' => function(){});
$this->router = new Router();
// Now verify whether the route has been skipped
$this->assertArrayNotHasKey('testInvalidParsing', $this->router->getRoutes());
}
/**
* @depends testVerbParsing
*/
public function testWildcardsParsing()
{
// Prepare the routes so they can be parsed
$config = Factory::getInstance()->config;
$config->routes->{'testWildcardsParsing/:any/:num'} = function(){};
$this->router = new Router();
// Now verify whether the route has been skipped
$this->assertArrayHasKey('testWildcardsParsing/[^/]+/[0-9]+', $this->router->getRoutes());
}
/* defaultCallable() -------------------------------------------------- */
/**
* @depends testGetRouterClass
*/
public function testDefaultCallable()
{
// First set the segments, callable and the route
$arguments = array('controller' => 'TestDefaultCallable', 'function' => 'index');
// Verify that the controller is found and loaded
$this->assertTrue($this->router->defaultCallable($arguments));
$this->assertInstanceOf('\Application\Controller\TestDefaultCallable', $this->router->getCallable());
}
/**
* @depends testDefaultCallable
*/
public function testDefaultCallableMissingMethod()
{
// First set the arguments
$this->assertEquals(200, Core::$http_status_code);
$arguments = array('controller' => 'TestDefaultCallableMissingMethod', 'function' => 'missing');
// Verify that the method is not found
$this->assertFalse($this->router->defaultCallable($arguments));
$this->assertEquals(404, Core::$http_status_code);
}
/**
* @depends testDefaultCallable
*/
public function testDefaultCallableMissingController()
{
// First set the arguments
$this->assertEquals(200, Core::$http_status_code);
$arguments = array('controller' => 'TestDefaultCallableMissingController', 'function' => 'index');
// Verify that the controller is not found
$this->assertFalse($this->router->defaultCallable($arguments));
$this->assertEquals(404, Core::$http_status_code);
}
/**
* @depends testDefaultCallable
*/
public function testDefaultCallableHalt()
{
// First set the arguments
$this->assertEquals(200, Core::$http_status_code);
$arguments = array('controller' => 'TestDefaultCallableHalt', 'function' => 'index');
// Verify that the controller is halted
$this->assertFalse($this->router->defaultCallable($arguments));
$this->assertEquals(200, Core::$http_status_code);
}
/* -------------------------------------------------------------------- */
/* route() ------------------------------------------------------------ */
/**
* @depends testGetRouterClass
*/
public function testRoute()
{
// First set the segments and the route
$uri = Factory::getInstance()->uri;
$uri->segments = array('testRoute');
$this->router->addRoute('testRoute', function(){});
// Perform verification
$this->assertFalse($this->router->route(false));
$this->assertContains('testRoute', $this->router->getMatches());
}
/**
* @depends testRoute
*/
public function testRouteDefaultRoute()
{
// First set the segments and the route
$uri = Factory::getInstance()->uri;
$uri->segments = array();
// Perform verification
$this->assertFalse($this->router->route(false));
$this->assertEmpty($this->router->getMatches());
}
/**
* @depends testRoute
*/
public function testArrayRoute()
{
// First set the segments and the route
$uri = Factory::getInstance()->uri;
$uri->segments = array('testArrayRoute');
$this->router->addRoute('testArrayRoute', array('callable' => function(){}));
// Perform verification
$this->assertTrue($this->router->route(false));
$this->assertContains('testArrayRoute', $this->router->getMatches());
}
/* -------------------------------------------------------------------- */
/* loadCallable() ----------------------------------------------------- */
/**
* @depends testArrayRoute
*/
public function testLoadCallable()
{
// First set the segments, callable and the route
$uri = Factory::getInstance()->uri;
$uri->segments = array('testLoadCallable');
$callable = function($arg){$this->assertEquals('testLoadCallable', $arg[0]);return true;};
$this->router->addRoute('testLoadCallable', array('callable' => $callable));
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertSame($callable, $this->router->getCallable());
}
/**
* @depends testLoadCallable
*/
public function testLoadCallableNotCallable()
{
// First set the segments, callable and the route
$uri = Factory::getInstance()->uri;
$uri->segments = array('testLoadCallableNotCallable');
$this->router->addRoute('testLoadCallableNotCallable', array('callable' => 'notCallable'));
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertSame('notCallable', $this->router->getCallable());
$this->assertEquals(500, Core::$http_status_code);
}
/**
* @depends testLoadCallable
*/
public function testLoadCallableToDefaultCallabe()
{
// First set the segments, callable and the route
$uri = Factory::getInstance()->uri;
$uri->segments = array('testLoadCallableToDefaultCallable');
$this->router->addRoute('testLoadCallableToDefaultCallable', array('callable' => array('controller' => 'TestLoadCallableToDefaultCallable', 'function' => 'index', 'parameters' => null)));
// Perform verification
$this->assertTrue($this->router->route(true));
}
public function testLoadCallableUnsatisfiedCallable()
{
// First set the segments, callable and the route
$uri = Factory::getInstance()->uri;
$uri->segments = array('testLoadCallableUnsatisfiedCallable');
$this->router->addRoute('testLoadCallableUnsatisfiedCallable', array('callable' => function() {return false;} ));
// Perform verification
$this->assertTrue($this->router->route(true));
}
/* -------------------------------------------------------------------- */
/* routeDefault() ----------------------------------------------------- */
/**
* @depends testLoadCallable
*/
public function testRouteDefault()
{
// First testing the default route without any segments
$uri = Factory::getInstance()->uri;
$uri->segments = array();
// Change the default controller so it is easier to test
$config = Factory::getInstance()->config;
$config->routing->default_controller = 'testRouteDefault';
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/**
* @depends testRouteDefault
*/
public function testRouteDefaultUriDashes()
{
// First testing the default route without any segments
$uri = Factory::getInstance()->uri;
// Change the default controller so URI Dashes get translated
$config = Factory::getInstance()->config;
$config->routing->translate_uri_dashes = TRUE;
// Perform verification, first test should fail
$uri->segments = array('test-Route-Default-Uri-Dashes-Fail');
$this->assertTrue($this->router->route(true));
$this->assertEquals(404, Core::$http_status_code);
// Reset the HTTP status code
Core::$http_status_code = 200;
// Next test should succeed
$uri->segments = array('test-Route-Default-Uri-Dashes-Succeed', 'dashed-method');
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/* -------------------------------------------------------------------- */
/* Simulation tests --------------------------------------------------- */
/**
* @depends testLoadCallable
*/
public function testLoadController()
{
// Adjust the URL
$uri = Factory::getInstance()->uri;
$uri->segments = array('testLoadController');
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/**
* @depends testLoadController
*/
public function testLoadStandardController()
{
// Adjust the URL
$uri = Factory::getInstance()->uri;
$uri->segments = array();
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/**
* @depends testLoadController
*/
public function testControllerNotFound()
{
// Adjust the URL
$uri = Factory::getInstance()->uri;
$uri->segments = array('controllerNotFound');
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertEquals(404, Core::$http_status_code);
}
/**
* @depends testLoadController
*/
public function testMatchingRoute()
{
// Adjust the URL
$uri = Factory::getInstance()->uri;
$uri->segments = array('methodInFirstArgument', 'testMatchingRoute');
// Create a custom route
$this->router->addRoute('^(?P<function>.*?)(|\/(?P<controller>.*?)(|\/(?P<parameters>.*?)))$', array('callable' => array($this->router, 'defaultCallable')));
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/**
* @depends testMatchingRoute
*/
public function testStaticRoute()
{
// Adjust the URL
$uri = Factory::getInstance()->uri;
$uri->segments = array('staticKey');
// Create a static route
$this->router->addRoute('staticKey', 'standard/index');
// Perform verification
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/**
* @depends testMatchingRoute
*/
public function testCustomCallable()
{
// Adjust the URL
$uri = Factory::getInstance()->uri;
$uri->segments = array('customCallable');
// Create a custom callable
$this->router->addRoute('customCallable', array('callable' => function($arguments){
return true;
} ));
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/**
* @depends testMatchingRoute
*/
public function testCustomRewriteCallable()
{
// Adjust the URL
$uri = Factory::getInstance()->uri;
$uri->segments = array('customRewriteCallable');
// Create a custom callable
$this->router->addRoute('customRewriteCallable', function($arguments = array()){
return 'standard/index';
});
$this->assertTrue($this->router->route(true));
$this->assertEquals(200, Core::$http_status_code);
}
/* -------------------------------------------------------------------- */
}

View File

@ -0,0 +1,70 @@
<?php
/**
* FuzeWorks.
*
* The FuzeWorks MVC PHP FrameWork
*
* Copyright (C) 2018 TechFuze
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2018, Techfuze. (http://techfuze.net)
* @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/)
* @license http://opensource.org/licenses/GPL-3.0 GPLv3 License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.1.4
*
* @version Version 1.1.4
*/
use FuzeWorks\Factory;
use FuzeWorks\Events;
use FuzeWorks\EventPriority;
/**
* Class pluginGetEventTest.
*/
class pluginGetEventTest extends CoreTestAbstract
{
/**
* Check if the event is fired when it should be.
*/
public function testPluginGetEvent()
{
// Create mock listener
Events::addListener(
function($event){$event->setCancelled(true);return $event;},
'pluginGetEvent',
EventPriority::NORMAL);
// And fire the event
$this->assertFalse(Factory::getInstance()->plugins->get('test'));
}
/**
* @depends testPluginGetEvent
*/
public function testReplacePlugin()
{
// Create mock listener
Events::addListener(
function($event){$event->setPlugin('test_string');return $event;},
'pluginGetEvent',
EventPriority::NORMAL);
// And fire the event
$this->assertEquals('test_string', Factory::getInstance()->plugins->get('test'));
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Plugin;
class ActualPlugin
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace FuzeWorks\Plugins;
use FuzeWorks\PluginInterface;
class ActualPluginHeader implements PluginInterface
{
public function init()
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Plugin;
class TestDisabledPlugin
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace FuzeWorks\Plugins;
use FuzeWorks\PluginInterface;
class TestDisabledPluginHeader implements PluginInterface
{
public function init()
{
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace FuzeWorks\Plugins;
use FuzeWorks\PluginInterface;
class TestGetPluginMethodHeader implements PluginInterface
{
public function init()
{
}
public function getPlugin()
{
return 'test_string';
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Plugin;
class TestSuperInvalidClass
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace FuzeWorks\Plugins;
use FuzeWorks\PluginInterface;
class TestInvalidClassHeader implements PluginInterface
{
public function init()
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Plugin;
class TestLoadPlugin
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace FuzeWorks\Plugins;
use FuzeWorks\PluginInterface;
class TestLoadPluginHeader implements PluginInterface
{
public function init()
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Plugin;
class TestMissingHeader
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace FuzeWorks\Plugins;
use FuzeWorks\PluginInterface;
class TestMissingPluginHeader implements PluginInterface
{
public function init()
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Plugin;
class TestReloadPlugin
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace FuzeWorks\Plugins;
use FuzeWorks\PluginInterface;
class TestReloadPluginHeader implements PluginInterface
{
public function init()
{
}
}