diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..b909682f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = false
+charset = utf-8
+indent_style = tab
+trim_trailing_whitespace = true
+
+[*.{cpp,c,h,hpp,cxx}]
+insert_final_newline = true
+
+# Yaml files
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 4
\ No newline at end of file
diff --git a/app/base/BaseApiModel.php b/app/Base/ApiModel.php
similarity index 95%
rename from app/base/BaseApiModel.php
rename to app/Base/ApiModel.php
index bc1edc87..0beb78b3 100644
--- a/app/base/BaseApiModel.php
+++ b/app/Base/ApiModel.php
@@ -2,7 +2,7 @@
/**
* Base API Model
*/
-namespace AnimeClient;
+namespace AnimeClient\Base;
use \GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar;
@@ -10,7 +10,7 @@ use \GuzzleHttp\Cookie\CookieJar;
/**
* Base model for api interaction
*/
-class BaseApiModel extends BaseModel {
+class ApiModel extends Model {
/**
* Base url for making api requests
diff --git a/app/base/Config.php b/app/Base/Config.php
similarity index 93%
rename from app/base/Config.php
rename to app/Base/Config.php
index c0e508a1..9ed8ff78 100644
--- a/app/base/Config.php
+++ b/app/Base/Config.php
@@ -1,6 +1,9 @@
__get('asset_path'), '/');
@@ -74,7 +77,7 @@ class Config {
* @param string $type - (optional) The controller
* @return string
*/
- function base_url($type="anime")
+ public function base_url($type="anime")
{
$config_path = trim($this->__get("{$type}_path"), "/");
$config_host = $this->__get("{$type}_host");
@@ -93,7 +96,7 @@ class Config {
* @param string $type - (optional) The controller (anime or manga), defaults to anime
* @return string
*/
- function full_url($path="", $type="anime")
+ public function full_url($path="", $type="anime")
{
$config_path = trim($this->__get("{$type}_path"), "/");
$config_host = $this->__get("{$type}_host");
diff --git a/app/base/BaseController.php b/app/Base/Controller.php
similarity index 99%
rename from app/base/BaseController.php
rename to app/Base/Controller.php
index 38dca981..910e5854 100644
--- a/app/base/BaseController.php
+++ b/app/Base/Controller.php
@@ -2,14 +2,14 @@
/**
* Base Controller
*/
-namespace AnimeClient;
+namespace AnimeClient\Base;
use Aura\Web\WebFactory;
/**
* Base class for controllers, defines output methods
*/
-class BaseController {
+class Controller {
/**
* The global configuration object
diff --git a/app/base/BaseDBModel.php b/app/Base/DBModel.php
similarity index 87%
rename from app/base/BaseDBModel.php
rename to app/Base/DBModel.php
index ae273509..d6b51a22 100644
--- a/app/base/BaseDBModel.php
+++ b/app/Base/DBModel.php
@@ -2,12 +2,12 @@
/**
* Base DB model
*/
-namespace AnimeClient;
+namespace AnimeClient\Base;
/**
* Base model for database interaction
*/
-class BaseDBModel extends BaseModel {
+class DBModel extends Model {
/**
* The query builder object
* @var object $db
diff --git a/app/base/BaseModel.php b/app/Base/Model.php
similarity index 98%
rename from app/base/BaseModel.php
rename to app/Base/Model.php
index 5de557ce..e0e167d3 100644
--- a/app/base/BaseModel.php
+++ b/app/Base/Model.php
@@ -2,14 +2,14 @@
/**
* Base for base models
*/
-namespace AnimeClient;
+namespace AnimeClient\Base;
use abeautifulsite\SimpleImage;
/**
* Common base for all Models
*/
-class BaseModel {
+class Model {
/**
* The global configuration object
diff --git a/app/base/Router.php b/app/Base/Router.php
similarity index 92%
rename from app/base/Router.php
rename to app/Base/Router.php
index beeb9804..754bf6bc 100644
--- a/app/base/Router.php
+++ b/app/Base/Router.php
@@ -3,7 +3,7 @@
* Routing logic
*/
-namespace AnimeClient;
+namespace AnimeClient\Base;
/**
* Basic routing/ dispatch
@@ -68,9 +68,9 @@ class Router {
$route_path = str_replace([$this->config->anime_path, $this->config->manga_path], '', $raw_route);
$route_path = "/" . trim($route_path, '/');
- $defaultHandler->addDataTable('Route Info', [
+ /*$defaultHandler->addDataTable('Route Info', [
'route_path' => $route_path
- ]);
+ ]);*/
$route = $this->router->match($route_path, $_SERVER);
@@ -107,15 +107,6 @@ class Router {
{
$failure = $this->router->getFailedRoute();
$defaultHandler->addDataTable('failed_route', (array)$failure);
-
- /*$controller_name = '\\AnimeClient\\BaseController';
- $action_method = 'outputHTML';
- $params = [
- 'template' => '404',
- 'data' => [
- 'title' => 'Page Not Found'
- ]
- ];*/
}
else
{
@@ -194,8 +185,8 @@ class Router {
public function _setup_routes()
{
$route_map = [
- 'anime' => '\\AnimeClient\\AnimeController',
- 'manga' => '\\AnimeClient\\MangaController',
+ 'anime' => '\\AnimeClient\\Controller\\Anime',
+ 'manga' => '\\AnimeClient\\Controller\\Manga',
];
$output_routes = [];
diff --git a/app/base/functions.php b/app/Base/functions.php
similarity index 100%
rename from app/base/functions.php
rename to app/Base/functions.php
diff --git a/app/base/pre_conf_functions.php b/app/Base/pre_conf_functions.php
similarity index 69%
rename from app/base/pre_conf_functions.php
rename to app/Base/pre_conf_functions.php
index b5b25dab..ab98a289 100644
--- a/app/base/pre_conf_functions.php
+++ b/app/Base/pre_conf_functions.php
@@ -26,18 +26,13 @@ function _setup_autoloaders()
require _dir(ROOT_DIR, '/vendor/autoload.php');
spl_autoload_register(function ($class) {
$class_parts = explode('\\', $class);
- $class = end($class_parts);
+ array_shift($class_parts);
+ $ns_path = APP_DIR . '/' . implode('/', $class_parts) . ".php";
- $dirs = ["base", "controllers", "models"];
-
- foreach($dirs as $dir)
+ if (file_exists($ns_path))
{
- $file = _dir(APP_DIR, $dir, "{$class}.php");
- if (file_exists($file))
- {
- require_once $file;
- return;
- }
+ require_once($ns_path);
+ return;
}
});
}
\ No newline at end of file
diff --git a/app/controllers/AnimeController.php b/app/Controller/Anime.php
similarity index 93%
rename from app/controllers/AnimeController.php
rename to app/Controller/Anime.php
index 7e8056a3..3b1541ab 100644
--- a/app/controllers/AnimeController.php
+++ b/app/Controller/Anime.php
@@ -3,12 +3,17 @@
* Anime Controller
*/
-namespace AnimeClient;
+namespace AnimeClient\Controller;
+
+use AnimeClient\Base\Controller as BaseController;
+use AnimeClient\Base\Config;
+use AnimeClient\Model\Anime as AnimeModel;
+use AnimeClient\Model\AnimeCollection as AnimeCollectionModel;
/**
* Controller for Anime-related pages
*/
-class AnimeController extends BaseController {
+class Anime extends BaseController {
/**
* The anime list model
diff --git a/app/controllers/MangaController.php b/app/Controller/Manga.php
similarity index 90%
rename from app/controllers/MangaController.php
rename to app/Controller/Manga.php
index 7f65dfe6..e23eb1b0 100644
--- a/app/controllers/MangaController.php
+++ b/app/Controller/Manga.php
@@ -2,12 +2,16 @@
/**
* Manga Controller
*/
-namespace AnimeClient;
+namespace AnimeClient\Controller;
+
+use AnimeClient\Base\Controller;
+use AnimeClient\Base\Config;
+use AnimeClient\Model\Manga as MangaModel;
/**
* Controller for manga list
*/
-class MangaController extends BaseController {
+class Manga extends Controller {
/**
* The manga model
diff --git a/app/models/AnimeModel.php b/app/Model/Anime.php
similarity index 97%
rename from app/models/AnimeModel.php
rename to app/Model/Anime.php
index e24a22c0..b7e2ab3d 100644
--- a/app/models/AnimeModel.php
+++ b/app/Model/Anime.php
@@ -3,12 +3,15 @@
* Anime API Model
*/
-namespace AnimeClient;
+namespace AnimeClient\Model;
+
+use AnimeClient\Base\ApiModel;
+use AnimeClient\Base\Config;
/**
* Model for handling requests dealing with the anime list
*/
-class AnimeModel extends BaseApiModel {
+class Anime extends ApiModel {
/**
* The base url for api requests
* @var string $base_url
diff --git a/app/models/AnimeCollectionModel.php b/app/Model/AnimeCollection.php
similarity index 97%
rename from app/models/AnimeCollectionModel.php
rename to app/Model/AnimeCollection.php
index 9ec45571..418b6e42 100644
--- a/app/models/AnimeCollectionModel.php
+++ b/app/Model/AnimeCollection.php
@@ -3,12 +3,16 @@
* Anime Collection DB Model
*/
-namespace AnimeClient;
+namespace AnimeClient\Model;
+
+use AnimeClient\Base\DBModel;
+use AnimeClient\Base\Config;
+use AnimeClient\Model\Anime as AnimeModel;
/**
* Model for getting anime collection data
*/
-class AnimeCollectionModel extends BaseDBModel {
+class AnimeCollection extends DBModel {
/**
* Anime API Model
diff --git a/app/models/MangaModel.php b/app/Model/Manga.php
similarity index 96%
rename from app/models/MangaModel.php
rename to app/Model/Manga.php
index b78da616..2fe270ac 100644
--- a/app/models/MangaModel.php
+++ b/app/Model/Manga.php
@@ -2,12 +2,15 @@
/**
* Manga API Model
*/
-namespace AnimeClient;
+namespace AnimeClient\Model;
+
+use AnimeClient\Base\ApiModel;
+use AnimeClient\Base\Config;
/**
* Model for handling requests dealing with the manga list
*/
-class MangaModel extends BaseApiModel {
+class Manga extends ApiModel {
/**
* The base url for api requests
diff --git a/app/bootstrap.php b/app/bootstrap.php
index 06f2ebcd..e9f621ae 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -1,4 +1,7 @@
pushHandler($jsonHandler);
$whoops->register();
+$container->set('error-handler', $defaultHandler);
+
// -----------------------------------------------------------------------------
// Injected Objects
// -----------------------------------------------------------------------------
// Create Config Object
-$config = new Config();
-require _dir(BASE_DIR, '/functions.php');
+$config = new Base\Config();
+$container->set('config', $config);
// Create Aura Router Object
$router_factory = new RouterFactory();
$aura_router = $router_factory->newInstance();
+$container->set('aura-router', $aura_router);
// Create Request/Response Objects
$web_factory = new WebFactory([
@@ -43,13 +58,13 @@ $web_factory = new WebFactory([
'_SERVER' => $_SERVER,
'_FILES' => $_FILES
]);
-$request = $web_factory->newRequest();
-$response = $web_factory->newResponse();
+$container->set('request', $web_factory->newRequest());
+$container->set('response', $web_factory->newResponse());
// -----------------------------------------------------------------------------
// Router
// -----------------------------------------------------------------------------
-$router = new Router($config, $aura_router, $request, $response);
+$router = new Base\Router($container);
$router->dispatch();
// End of bootstrap.php
\ No newline at end of file
diff --git a/app/config/config.php b/app/config/config.php
index c1366c61..040982c9 100644
--- a/app/config/config.php
+++ b/app/config/config.php
@@ -1,4 +1,4 @@
- TRUE,
- // path to public directory
- 'asset_path' => '//' . $_SERVER['HTTP_HOST'] . '/public',
-
// path to public directory on the server
'asset_dir' => __DIR__ . '/../../public',
// ----------------------------------------------------------------------------
// Routing
- //
- // Route by path, or route by domain. To route by path, set the _host suffixed
- // options to an empty string, and set 'route_by' to 'path'. To route by host, set
- // the _path suffixed options to an empty string, and set 'route_by' to 'host'.
// ----------------------------------------------------------------------------
- 'route_by' => 'path', // host or path
- 'anime_host' => '',
- 'manga_host' => '',
+ 'routing' => [
+ // Subfolder prefix for url
+ 'subfolder_prefix' => '',
+
+ // Path to public directory, where images/css/javascript are located,
+ // appended to the url
+ 'asset_path' => '/public',
+
+ // Url paths to each content type
+ 'anime_path' => 'anime',
+ 'manga_path' => 'manga',
+ 'collection_path' => 'collection',
+ 'stats_path' => 'stats',
+
+ // Which list should be the default?
+ 'default_list' => 'anime', // anime or manga
+
+ // Default pages for anime/manga
+ 'default_anime_path' => "/anime/watching",
+ 'default_manga_path' => '/manga/all',
+
+ // Default to list view?
+ 'default_to_list_view' => FALSE,
+ ],
+
+ // Url paths to each
'anime_path' => 'anime',
'manga_path' => 'manga',
+ 'collection_path' => 'collection',
+ 'stats_path' => 'stats',
// Which list should be the default?
'default_list' => 'anime', // anime or manga
// Default pages for anime/manga
- 'default_anime_path' => '/watching',
- 'default_manga_path' => '/all',
+ 'default_anime_path' => "/anime/watching",
+ 'default_manga_path' => '/manga/all',
// Default to list view?
'default_to_list_view' => FALSE,
diff --git a/app/config/routes.php b/app/config/routes.php
index 02c4241f..92e46c47 100644
--- a/app/config/routes.php
+++ b/app/config/routes.php
@@ -22,6 +22,43 @@ return [
'path' => '/logout',
'action' => ['logout']
],
+ ],
+ // Routes on collection controller
+ 'collection' => [
+ 'collection_add_form' => [
+ 'path' => '/collection/add',
+ 'action' => ['form'],
+ 'params' => [],
+ ],
+ 'collection_edit_form' => [
+ 'path' => '/collection/edit/{id}',
+ 'action' => ['form'],
+ 'tokens' => [
+ 'id' => '[0-9]+'
+ ]
+ ],
+ 'collection_add' => [
+ 'path' => '/collection/add',
+ 'action' => ['add'],
+ 'verb' => 'post'
+ ],
+ 'collection_edit' => [
+ 'path' => '/collection/edit',
+ 'action' => ['edit'],
+ 'verb' => 'post'
+ ],
+ 'collection' => [
+ 'path' => '/collection/view{/view}',
+ 'action' => ['index'],
+ 'params' => [],
+ 'tokens' => [
+ 'view' => '[a-z_]+'
+ ]
+ ],
+ ],
+ // Routes on stats controller
+ 'stats' => [
+
],
// Routes on anime controller
'anime' => [
@@ -34,11 +71,11 @@ return [
]
],
'search' => [
- 'path' => '/search',
+ 'path' => '/anime/search',
'action' => ['search'],
],
'all' => [
- 'path' => '/all{/view}',
+ 'path' => '/anime/all{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'all',
@@ -49,7 +86,7 @@ return [
]
],
'watching' => [
- 'path' => '/watching{/view}',
+ 'path' => '/anime/watching{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'currently-watching',
@@ -60,7 +97,7 @@ return [
]
],
'plan_to_watch' => [
- 'path' => '/plan_to_watch{/view}',
+ 'path' => '/anime/plan_to_watch{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'plan-to-watch',
@@ -71,7 +108,7 @@ return [
]
],
'on_hold' => [
- 'path' => '/on_hold{/view}',
+ 'path' => '/anime/on_hold{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'on-hold',
@@ -82,7 +119,7 @@ return [
]
],
'dropped' => [
- 'path' => '/dropped{/view}',
+ 'path' => '/anime/dropped{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'dropped',
@@ -93,7 +130,7 @@ return [
]
],
'completed' => [
- 'path' => '/completed{/view}',
+ 'path' => '/anime/completed{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'completed',
@@ -103,36 +140,6 @@ return [
'view' => '[a-z_]+'
]
],
- 'collection_add_form' => [
- 'path' => '/collection/add',
- 'action' => ['collection_form'],
- 'params' => [],
- ],
- 'collection_edit_form' => [
- 'path' => '/collection/edit/{id}',
- 'action' => ['collection_form'],
- 'tokens' => [
- 'id' => '[0-9]+'
- ]
- ],
- 'collection_add' => [
- 'path' => '/collection/add',
- 'action' => ['collection_add'],
- 'verb' => 'post'
- ],
- 'collection_edit' => [
- 'path' => '/collection/edit',
- 'action' => ['collection_edit'],
- 'verb' => 'post'
- ],
- 'collection' => [
- 'path' => '/collection/view{/view}',
- 'action' => ['collection'],
- 'params' => [],
- 'tokens' => [
- 'view' => '[a-z_]+'
- ]
- ],
],
'manga' => [
'index' => [
@@ -145,7 +152,7 @@ return [
]
],
'all' => [
- 'path' => '/all{/view}',
+ 'path' => '/manga/all{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'all',
@@ -156,7 +163,7 @@ return [
]
],
'reading' => [
- 'path' => '/reading{/view}',
+ 'path' => '/manga/reading{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Reading',
@@ -167,7 +174,7 @@ return [
]
],
'plan_to_read' => [
- 'path' => '/plan_to_read{/view}',
+ 'path' => '/manga/plan_to_read{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Plan to Read',
@@ -178,7 +185,7 @@ return [
]
],
'on_hold' => [
- 'path' => '/on_hold{/view}',
+ 'path' => '/manga/on_hold{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'On Hold',
@@ -189,7 +196,7 @@ return [
]
],
'dropped' => [
- 'path' => '/dropped{/view}',
+ 'path' => '/manga/dropped{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Dropped',
@@ -200,7 +207,7 @@ return [
]
],
'completed' => [
- 'path' => '/completed{/view}',
+ 'path' => '/manga/completed{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Completed',
diff --git a/composer.json b/composer.json
index 49ee5c41..cddb03de 100644
--- a/composer.json
+++ b/composer.json
@@ -1,11 +1,17 @@
{
+ "name": "timw4mail/hummingbird-anime-client",
+ "description": "A self-hosted anime/manga client for hummingbird.",
+ "license":"MIT",
"require": {
"guzzlehttp/guzzle": "5.3.*",
- "filp/whoops": "1.1.*",
+ "filp/whoops": "dev-php7#fe32a402b086b21360e82013e8a0355575c7c6f4",
"aura/router": "2.2.*",
"aura/web": "2.0.*",
- "aviat4ion/query": "2.0.*",
- "robmorgan/phinx": "*",
- "abeautifulsite/simpleimage": "*"
+ "aura/html": "2.*",
+ "aura/session": "2.*",
+ "aviat4ion/query": "2.5.*",
+ "robmorgan/phinx": "0.4.*",
+ "abeautifulsite/simpleimage": "2.5.*",
+ "szymach/c-pchart": "1.*"
}
}
\ No newline at end of file
diff --git a/index.php b/index.php
index 4e9cdc85..87e8c089 100644
--- a/index.php
+++ b/index.php
@@ -3,8 +3,6 @@
* Here begins everything!
*/
-namespace AnimeClient;
-
// -----------------------------------------------------------------------------
// ! Start config
// -----------------------------------------------------------------------------
@@ -30,9 +28,42 @@ if ($timezone === '' || $timezone === FALSE)
// Define base directories
define('ROOT_DIR', __DIR__);
define('APP_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'app');
+define('SRC_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'src');
define('CONF_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'config');
-define('BASE_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'base');
-require BASE_DIR . DIRECTORY_SEPARATOR . 'pre_conf_functions.php';
+define('BASE_DIR', SRC_DIR . DIRECTORY_SEPARATOR . 'Base');
+
+/**
+ * Joins paths together. Variadic to take an
+ * arbitrary number of arguments
+ *
+ * @return string
+ */
+function _dir()
+{
+ return implode(DIRECTORY_SEPARATOR, func_get_args());
+}
+
+/**
+ * Set up autoloaders
+ *
+ * @codeCoverageIgnore
+ * @return void
+ */
+function _setup_autoloaders()
+{
+ require _dir(ROOT_DIR, '/vendor/autoload.php');
+ spl_autoload_register(function ($class) {
+ $class_parts = explode('\\', $class);
+ array_shift($class_parts);
+ $ns_path = SRC_DIR . '/' . implode('/', $class_parts) . ".php";
+
+ if (file_exists($ns_path))
+ {
+ require_once($ns_path);
+ return;
+ }
+ });
+}
// Setup autoloaders
_setup_autoloaders();
diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml
index 7dd06d43..49c474b6 100644
--- a/phpdoc.dist.xml
+++ b/phpdoc.dist.xml
@@ -11,13 +11,7 @@
- .
- app
- public/*
- app/views/*
- app/config/*
- migrations/*
- tests/*
- vendor/*
+ src
+ src/views/*
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
index 9c9a3125..3892c6ae 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -5,15 +5,18 @@
bootstrap="tests/bootstrap.php">
- app/base
- app/controllers
- app/models
+ src/Base
+ src/Controller
+ src/Model
- tests/base
+ tests
+ tests/Base
+ tests/Model
+ tests/Controller
diff --git a/src/Base/Config.php b/src/Base/Config.php
new file mode 100644
index 00000000..fc5c3654
--- /dev/null
+++ b/src/Base/Config.php
@@ -0,0 +1,164 @@
+config = array_merge($config, $base_config);
+ }
+ else // @codeCoverageIgnoreEnd
+ {
+ $this->config = $config_files;
+ }
+ }
+
+ /**
+ * Getter for config values
+ *
+ * @param string $key
+ * @return mixed
+ */
+ public function __get($key)
+ {
+ if (isset($this->config[$key]))
+ {
+ return $this->config[$key];
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Get the base url for css/js/images
+ *
+ * @return string
+ */
+ public function asset_url(/*...*/)
+ {
+ $args = func_get_args();
+ $base_url = rtrim($this->url(""), '/');
+
+ $routing_config = $this->__get("routing");
+
+
+ $base_url = "{$base_url}" . $routing_config['asset_path'];
+
+ array_unshift($args, $base_url);
+
+ return implode("/", $args);
+ }
+
+ /**
+ * Get the base url from the config
+ *
+ * @param string $type - (optional) The controller
+ * @return string
+ */
+ public function base_url($type="anime")
+ {
+ $config_path = trim($this->__get("{$type}_path"), "/");
+
+ // Set the appropriate HTTP host
+ $host = $_SERVER['HTTP_HOST'];
+ $path = ($config_path !== '') ? $config_path : "";
+
+ return implode("/", ['/', $host, $path]);
+ }
+
+ /**
+ * Generate a proper url from the path
+ *
+ * @param string $path
+ * @return string
+ */
+ public function url($path)
+ {
+ $path = trim($path, '/');
+
+ // Remove any optional parameters from the route
+ $path = preg_replace('`{/.*?}`i', '', $path);
+
+ // Set the appropriate HTTP host
+ $host = $_SERVER['HTTP_HOST'];
+
+ return "//{$host}/{$path}";
+ }
+
+ public function default_url($type)
+ {
+ $type = trim($type);
+ $default_path = $this->__get("default_{$type}_path");
+
+ if ( ! is_null($default_path))
+ {
+ return $this->url($default_path);
+ }
+
+ return "";
+ }
+
+ /**
+ * Generate full url path from the route path based on config
+ *
+ * @param string $path - (optional) The route path
+ * @param string $type - (optional) The controller (anime or manga), defaults to anime
+ * @return string
+ */
+ public function full_url($path="", $type="anime")
+ {
+ $config_path = trim($this->__get("{$type}_path"), "/");
+ $config_default_route = $this->__get("default_{$type}_path");
+
+ // Remove beginning/trailing slashes
+ $config_path = trim($config_path, '/');
+ $path = trim($path, '/');
+
+ // Remove any optional parameters from the route
+ $path = preg_replace('`{/.*?}`i', '', $path);
+
+ // Set the appropriate HTTP host
+ $host = $_SERVER['HTTP_HOST'];
+
+ // Set the default view
+ if ($path === '')
+ {
+ $path .= trim($config_default_route, '/');
+ if ($this->__get('default_to_list_view')) $path .= '/list';
+ }
+
+ // Set an leading folder
+ /*if ($config_path !== '')
+ {
+ $path = "{$config_path}/{$path}";
+ }*/
+
+ return "//{$host}/{$path}";
+ }
+}
+// End of config.php
\ No newline at end of file
diff --git a/src/Base/Container.php b/src/Base/Container.php
new file mode 100644
index 00000000..e267a472
--- /dev/null
+++ b/src/Base/Container.php
@@ -0,0 +1,50 @@
+container = $values;
+ }
+
+ /**
+ * Get a value
+ *
+ * @param string $key
+ * @retun mixed
+ */
+ public function get($key)
+ {
+ if (array_key_exists($key, $this->container))
+ {
+ return $this->container[$key];
+ }
+ }
+
+ /**
+ * Add a value to the container
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return Container
+ */
+ public function set($key, $value)
+ {
+ $this->container[$key] = $value;
+ return $this;
+ }
+}
+// End of Container.php
\ No newline at end of file
diff --git a/src/Base/Controller.php b/src/Base/Controller.php
new file mode 100644
index 00000000..f1669d59
--- /dev/null
+++ b/src/Base/Controller.php
@@ -0,0 +1,283 @@
+ 'anime',
+ 'other_type' => 'manga',
+ 'nav_routes' => []
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param Config $config
+ * @param array $web
+ */
+ public function __construct(Container $container)
+ {
+ $this->config = $container->get('config');
+ $this->base_data['config'] = $this->config;
+
+ $this->request = $container->get('request');
+ $this->response = $container->get('response');
+ }
+
+ /**
+ * Destructor
+ *
+ * @codeCoverageIgnore
+ */
+ public function __destruct()
+ {
+ $this->output();
+ }
+
+ /**
+ * Get a class member
+ *
+ * @param string $key
+ * @return object
+ */
+ public function __get($key)
+ {
+ $allowed = ['request', 'response', 'config'];
+
+ if (in_array($key, $allowed))
+ {
+ return $this->$key;
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Get the string output of a partial template
+ *
+ * @codeCoverageIgnore
+ * @param string $template
+ * @param array|object $data
+ * @return string
+ */
+ public function load_partial($template, $data=[])
+ {
+ if (isset($this->base_data))
+ {
+ $data = array_merge($this->base_data, $data);
+ }
+
+ global $router, $defaultHandler;
+ $route = $router->get_route();
+ $data['route_path'] = ($route) ? $router->get_route()->path : "";
+
+ $defaultHandler->addDataTable('Template Data', $data);
+
+ $template_path = _dir(SRC_DIR, 'views', "{$template}.php");
+
+ if ( ! is_file($template_path))
+ {
+ throw new \InvalidArgumentException("Invalid template : {$path}");
+ }
+
+ ob_start();
+ extract($data);
+ include _dir(SRC_DIR, 'views', 'header.php');
+ include $template_path;
+ include _dir(SRC_DIR, 'views', 'footer.php');
+ $buffer = ob_get_contents();
+ ob_end_clean();
+
+ return $buffer;
+ }
+
+ /**
+ * Output a template to HTML, using the provided data
+ *
+ * @codeCoverageIgnore
+ * @param string $template
+ * @param array|object $data
+ * @return void
+ */
+ public function outputHTML($template, $data=[])
+ {
+ $buffer = $this->load_partial($template, $data);
+
+ $this->response->content->setType('text/html');
+ $this->response->content->set($buffer);
+ }
+
+ /**
+ * Output json with the proper content type
+ *
+ * @param mixed $data
+ * @return void
+ */
+ public function outputJSON($data)
+ {
+ if ( ! is_string($data))
+ {
+ $data = json_encode($data);
+ }
+
+ $this->response->content->setType('application/json');
+ $this->response->content->set($data);
+ }
+
+ /**
+ * Redirect to the selected page
+ *
+ * @codeCoverageIgnore
+ * @param string $url
+ * @param int $code
+ * @param string $type
+ * @return void
+ */
+ public function redirect($url, $code, $type="anime")
+ {
+ $url = $this->config->full_url($url, $type);
+
+ $this->response->redirect->to($url, $code);
+ }
+
+ /**
+ * Add a message box to the page
+ *
+ * @codeCoverageIgnore
+ * @param string $type
+ * @param string $message
+ * @return string
+ */
+ public function show_message($type, $message)
+ {
+ return $this->load_partial('message', [
+ 'stat_class' => $type,
+ 'message' => $message
+ ]);
+ }
+
+ /**
+ * Clear the api session
+ *
+ * @codeCoverageIgnore
+ * @return void
+ */
+ public function logout()
+ {
+ session_destroy();
+ $this->response->redirect->seeOther($this->config->full_url(''));
+ }
+
+ /**
+ * Show the login form
+ *
+ * @codeCoverageIgnore
+ * @param string $status
+ * @return void
+ */
+ public function login($status="")
+ {
+ $message = "";
+
+ if ($status != "")
+ {
+ $message = $this->show_message('error', $status);
+ }
+
+ $this->outputHTML('login', [
+ 'title' => 'Api login',
+ 'message' => $message
+ ]);
+ }
+
+ /**
+ * Attempt to log in with the api
+ *
+ * @return void
+ */
+ public function login_action()
+ {
+ if (
+ $this->model->authenticate(
+ $this->config->hummingbird_username,
+ $this->request->post->get('password')
+ )
+ )
+ {
+ $this->response->redirect->afterPost($this->config->full_url('', $this->base_data['url_type']));
+ return;
+ }
+
+ $this->login("Invalid username or password.");
+ }
+
+ /**
+ * Send the appropriate response
+ *
+ * @codeCoverageIgnore
+ * @return void
+ */
+ private function output()
+ {
+ // send status
+ @header($this->response->status->get(), true, $this->response->status->getCode());
+
+ // headers
+ foreach($this->response->headers->get() as $label => $value)
+ {
+ @header("{$label}: {$value}");
+ }
+
+ // cookies
+ foreach($this->response->cookies->get() as $name => $cookie)
+ {
+ @setcookie(
+ $name,
+ $cookie['value'],
+ $cookie['expire'],
+ $cookie['path'],
+ $cookie['domain'],
+ $cookie['secure'],
+ $cookie['httponly']
+ );
+ }
+
+ // send the actual response
+ echo $this->response->content->get();
+ }
+}
+// End of BaseController.php
diff --git a/src/Base/Model.php b/src/Base/Model.php
new file mode 100644
index 00000000..76896170
--- /dev/null
+++ b/src/Base/Model.php
@@ -0,0 +1,112 @@
+container = $container;
+ $this->config = $container->get('config');
+ }
+
+ /**
+ * Get the path of the cached version of the image. Create the cached image
+ * if the file does not already exist
+ *
+ * @codeCoverageIgnore
+ * @param string $api_path - The original image url
+ * @param string $series_slug - The part of the url with the series name, becomes the image name
+ * @param string $type - Anime or Manga, controls cache path
+ * @return string - the frontend path for the cached image
+ */
+ public function get_cached_image($api_path, $series_slug, $type="anime")
+ {
+ $api_path = str_replace("jjpg", "jpg", $api_path);
+ $path_parts = explode('?', basename($api_path));
+ $path = current($path_parts);
+ $ext_parts = explode('.', $path);
+ $ext = end($ext_parts);
+
+ // Workaround for some broken extensions
+ if ($ext == "jjpg") $ext = "jpg";
+
+ // Failsafe for weird urls
+ if (strlen($ext) > 3) return $api_path;
+
+ $cached_image = "{$series_slug}.{$ext}";
+ $cached_path = "{$this->config->img_cache_path}/{$type}/{$cached_image}";
+
+ // Cache the file if it doesn't already exist
+ if ( ! file_exists($cached_path))
+ {
+ if (ini_get('allow_url_fopen'))
+ {
+ copy($api_path, $cached_path);
+ }
+ elseif (function_exists('curl_init'))
+ {
+ $ch = curl_init($api_path);
+ $fp = fopen($cached_path, 'wb');
+ curl_setopt_array($ch, [
+ CURLOPT_FILE => $fp,
+ CURLOPT_HEADER => 0
+ ]);
+ curl_exec($ch);
+ curl_close($ch);
+ fclose($ch);
+ }
+ else
+ {
+ throw new DomainException("Couldn't cache images because they couldn't be downloaded.");
+ }
+
+ // Resize the image
+ if ($type == 'anime')
+ {
+ $resize_width = 220;
+ $resize_height = 319;
+ $this->_resize($cached_path, $resize_width, $resize_height);
+ }
+ }
+
+ return "/public/images/{$type}/{$cached_image}";
+ }
+
+ /**
+ * Resize an image
+ *
+ * @codeCoverageIgnore
+ * @param string $path
+ * @param string $width
+ * @param string $height
+ */
+ private function _resize($path, $width, $height)
+ {
+ $img = new SimpleImage($path);
+ $img->resize($width,$height)->save();
+ }
+}
+// End of BaseModel.php
\ No newline at end of file
diff --git a/src/Base/Model/API.php b/src/Base/Model/API.php
new file mode 100644
index 00000000..b8a2f748
--- /dev/null
+++ b/src/Base/Model/API.php
@@ -0,0 +1,81 @@
+cookieJar = new CookieJar();
+ $this->client = new Client([
+ 'base_url' => $this->base_url,
+ 'defaults' => [
+ 'cookies' => $this->cookieJar,
+ 'headers' => [
+ 'User-Agent' => $_SERVER['HTTP_USER_AGENT'],
+ 'Accept-Encoding' => 'application/json'
+ ],
+ 'timeout' => 5,
+ 'connect_timeout' => 5
+ ]
+ ]);
+ }
+
+ /**
+ * Attempt login via the api
+ *
+ * @codeCoverageIgnore
+ * @param string $username
+ * @param string $password
+ * @return bool
+ */
+ public function authenticate($username, $password)
+ {
+ $result = $this->client->post('https://hummingbird.me/api/v1/users/authenticate', [
+ 'body' => [
+ 'username' => $username,
+ 'password' => $password
+ ]
+ ]);
+
+ if ($result->getStatusCode() === 201)
+ {
+ $_SESSION['hummingbird_anime_token'] = $result->json();
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+}
+// End of BaseApiModel.php
\ No newline at end of file
diff --git a/src/Base/Model/DB.php b/src/Base/Model/DB.php
new file mode 100644
index 00000000..7992e91b
--- /dev/null
+++ b/src/Base/Model/DB.php
@@ -0,0 +1,34 @@
+db_config = $this->config->database;
+ }
+}
+// End of BaseDBModel.php
\ No newline at end of file
diff --git a/src/Base/Router.php b/src/Base/Router.php
new file mode 100644
index 00000000..9c7be4bb
--- /dev/null
+++ b/src/Base/Router.php
@@ -0,0 +1,230 @@
+config = $container->get('config');
+ $this->router = $container->get('aura-router');
+ $this->request = $container->get('request');
+ $this->web = [$this->request, $container->get('response')];
+
+ $this->output_routes = $this->_setup_routes();
+
+ $this->container = $container;
+ }
+
+ /**
+ * Get the current route object, if one matches
+ *
+ * @return object
+ */
+ public function get_route()
+ {
+ $error_handler = $this->container->get('error-handler');
+
+ $raw_route = $this->request->server->get('PATH_INFO');
+ $route_path = "/" . trim($raw_route, '/');
+
+ $error_handler->addDataTable('Route Info', [
+ 'route_path' => $route_path
+ ]);
+
+ $route = $this->router->match($route_path, $_SERVER);
+
+ return $route;
+ }
+
+ /**
+ * Get list of routes applied
+ *
+ * @return array
+ */
+ public function get_output_routes()
+ {
+ return $this->output_routes;
+ }
+
+ /**
+ * Handle the current route
+ *
+ * @codeCoverageIgnore
+ * @param [object] $route
+ * @return void
+ */
+ public function dispatch($route = NULL)
+ {
+ $error_handler = $this->container->get('error-handler');
+
+ if (is_null($route))
+ {
+ $route = $this->get_route();
+ $error_handler->addDataTable('route_args', (array)$route);
+ }
+
+ if ( ! $route)
+ {
+ $failure = $this->router->getFailedRoute();
+ $error_handler->addDataTable('failed_route', (array)$failure);
+ }
+ else
+ {
+ list($controller_name, $action_method) = $route->params['action'];
+ $params = (isset($route->params['params'])) ? $route->params['params'] : [];
+
+ if ( ! empty($route->tokens))
+ {
+ foreach($route->tokens as $key => $v)
+ {
+ if (array_key_exists($key, $route->params))
+ {
+ $params[$key] = $route->params[$key];
+ }
+ }
+ }
+ }
+
+ $controller = new $controller_name($this->container);
+
+ // Run the appropriate controller method
+
+ $error_handler->addDataTable('controller_args', $params);
+ call_user_func_array([$controller, $action_method], $params);
+ }
+
+ /**
+ * Get the type of route, to select the current controller
+ *
+ * @return string
+ */
+ public function get_controller()
+ {
+ $route_type = $this->config->default_list;
+
+ $host = $this->request->server->get("HTTP_HOST");
+ $request_uri = $this->request->server->get('PATH_INFO');
+
+ $path = trim($request_uri, '/');
+
+ $route_type_map = [
+ $this->config->anime_path => 'anime',
+ $this->config->manga_path => 'manga',
+ $this->config->collection_path => 'collection',
+ $this->config->stats_path => 'stats'
+ ];
+
+ $segments = explode('/', $path);
+ $controller = array_shift($segments);
+
+ if (array_key_exists($controller, array_keys($route_type_map)))
+ {
+ return $route_type_map[$controller];
+ }
+
+ return $route_type;
+ }
+
+ /**
+ * Select controller based on the current url, and apply its relevent routes
+ *
+ * @return array
+ */
+ public function _setup_routes()
+ {
+ $output_routes = [];
+
+ $route_type = $this->get_controller();
+
+ // Return early if invalid route array
+ if ( ! array_key_exists($route_type, $this->config->routes)) return [];
+
+ $applied_routes = array_merge($this->config->routes[$route_type], $this->config->routes['common']);
+
+ // Add routes
+ foreach($applied_routes as $name => &$route)
+ {
+ $path = $route['path'];
+ unset($route['path']);
+
+ $controller_class = '\\AnimeClient\\Controller\\' . ucfirst($route_type);
+
+ // Prepend the controller to the route parameters
+ array_unshift($route['action'], $controller_class);
+
+ // Select the appropriate router method based on the http verb
+ $add = (array_key_exists('verb', $route)) ? "add" . ucfirst(strtolower($route['verb'])) : "addGet";
+
+ // Add the route to the router object
+ if ( ! array_key_exists('tokens', $route))
+ {
+ $output_routes[] = $this->router->$add($name, $path)->addValues($route);
+ }
+ else
+ {
+ $tokens = $route['tokens'];
+ unset($route['tokens']);
+
+ $output_routes[] = $this->router->$add($name, $path)
+ ->addValues($route)
+ ->addTokens($tokens);
+ }
+ }
+
+ return $output_routes;
+ }
+}
+// End of Router.php
\ No newline at end of file
diff --git a/src/Base/UrlGenerator.php b/src/Base/UrlGenerator.php
new file mode 100644
index 00000000..a7ccdacd
--- /dev/null
+++ b/src/Base/UrlGenerator.php
@@ -0,0 +1,149 @@
+config = $container->get('config');
+ }
+
+ /**
+ * Retreive the appropriate value for the routing key
+ *
+ * @param string $key
+ * @return mixed
+ */
+ protected function __get($key)
+ {
+ $routing_config = $this->config->__get('routing');
+
+ if (array_key_exists($key, $routing_config))
+ {
+ return $routing_config[$key];
+ }
+ }
+
+ public function __invoke()
+ {
+ $args = func_get_args();
+ }
+
+ /**
+ * Get the base url for css/js/images
+ *
+ * @return string
+ */
+ public function asset_url(/*...*/)
+ {
+ $args = func_get_args();
+ $base_url = rtrim($this->__get('asset_path'), '/');
+
+ array_unshift($args, $base_url);
+
+ return implode("/", $args);
+ }
+
+ /**
+ * Get the base url from the config
+ *
+ * @param string $type - (optional) The controller
+ * @return string
+ */
+ public function base_url($type="anime")
+ {
+ $config_path = trim($this->__get("{$type}_path"), "/");
+
+ // Set the appropriate HTTP host
+ $host = $_SERVER['HTTP_HOST'];
+ $path = ($config_path !== '') ? $config_path : "";
+
+ return implode("/", ['/', $host, $path]);
+ }
+
+ /**
+ * Generate a proper url from the path
+ *
+ * @param string $path
+ * @return string
+ */
+ public function url($path)
+ {
+ $path = trim($path, '/');
+
+ // Remove any optional parameters from the route
+ $path = preg_replace('`{/.*?}`i', '', $path);
+
+ // Set the appropriate HTTP host
+ $host = $_SERVER['HTTP_HOST'];
+
+ return "//{$host}/{$path}";
+ }
+
+ public function default_url($type)
+ {
+ $type = trim($type);
+ $default_path = $this->__get("default_{$type}_path");
+
+ if ( ! is_null($default_path))
+ {
+ return $this->url($default_path);
+ }
+
+ return "";
+ }
+
+ /**
+ * Generate full url path from the route path based on config
+ *
+ * @param string $path - (optional) The route path
+ * @param string $type - (optional) The controller (anime or manga), defaults to anime
+ * @return string
+ */
+ public function full_url($path="", $type="anime")
+ {
+ $config_path = trim($this->__get("{$type}_path"), "/");
+ $config_default_route = $this->__get("default_{$type}_path");
+
+ // Remove beginning/trailing slashes
+ $config_path = trim($config_path, '/');
+ $path = trim($path, '/');
+
+ // Remove any optional parameters from the route
+ $path = preg_replace('`{/.*?}`i', '', $path);
+
+ // Set the appropriate HTTP host
+ $host = $_SERVER['HTTP_HOST'];
+
+ // Set the default view
+ if ($path === '')
+ {
+ $path .= trim($config_default_route, '/');
+ if ($this->__get('default_to_list_view')) $path .= '/list';
+ }
+
+ // Set an leading folder
+ /*if ($config_path !== '')
+ {
+ $path = "{$config_path}/{$path}";
+ }*/
+
+ return "//{$host}/{$path}";
+ }
+}
+// End of UrlGenerator.php
\ No newline at end of file
diff --git a/src/Controller/Anime.php b/src/Controller/Anime.php
new file mode 100644
index 00000000..ac2a261b
--- /dev/null
+++ b/src/Controller/Anime.php
@@ -0,0 +1,197 @@
+ '/anime/watching{/view}',
+ 'Plan to Watch' => '/anime/plan_to_watch{/view}',
+ 'On Hold' => '/anime/on_hold{/view}',
+ 'Dropped' => '/anime/dropped{/view}',
+ 'Completed' => '/anime/completed{/view}',
+ 'Collection' => '/collection/view{/view}',
+ 'All' => '/anime/all{/view}'
+ ];
+
+ /**
+ * Constructor
+ */
+ public function __construct(Container $container)
+ {
+ parent::__construct($container);
+
+ $config = $container->get('config');
+
+ if ($this->config->show_anime_collection === FALSE)
+ {
+ unset($this->nav_routes['Collection']);
+ }
+
+ $this->model = new AnimeModel($container);
+ $this->collection_model = new AnimeCollectionModel($container);
+ $this->base_data = [
+ 'message' => '',
+ 'url_type' => 'anime',
+ 'other_type' => 'manga',
+ 'nav_routes' => $this->nav_routes,
+ 'config' => $this->config,
+ ];
+ }
+
+ /**
+ * Search for anime
+ *
+ * @return void
+ */
+ public function search()
+ {
+ $query = $this->request->query->get('query');
+ $this->outputJSON($this->model->search($query));
+ }
+
+ /**
+ * Show a portion, or all of the anime list
+ *
+ * @param string $type - The section of the list
+ * @param string $title - The title of the page
+ * @return void
+ */
+ public function anime_list($type, $title, $view)
+ {
+ $view_map = [
+ '' => 'cover',
+ 'list' => 'list'
+ ];
+
+ $data = ($type != 'all')
+ ? $this->model->get_list($type)
+ : $this->model->get_all_lists();
+
+ $this->outputHTML('anime/' . $view_map[$view], [
+ 'title' => $title,
+ 'sections' => $data
+ ]);
+ }
+
+ /**
+ * Show the anime collection page
+ *
+ * @return void
+ */
+ public function collection($view)
+ {
+ $view_map = [
+ '' => 'collection',
+ 'list' => 'collection_list'
+ ];
+
+ $data = $this->collection_model->get_collection();
+
+ $this->outputHTML('anime/' . $view_map[$view], [
+ 'title' => WHOSE . " Anime Collection",
+ 'sections' => $data,
+ 'genres' => $this->collection_model->get_genre_list()
+ ]);
+ }
+
+ /**
+ * Show the anime collection add/edit form
+ *
+ * @param int $id
+ * @return void
+ */
+ public function collection_form($id=NULL)
+ {
+ $action = (is_null($id)) ? "Add" : "Edit";
+
+ $this->outputHTML('anime/collection_' . strtolower($action), [
+ 'action' => $action,
+ 'action_url' => $this->config->full_url("collection/" . strtolower($action)),
+ 'title' => WHOSE . " Anime Collection · {$action}",
+ 'media_items' => $this->collection_model->get_media_type_list(),
+ 'item' => ($action === "Edit") ? $this->collection_model->get($id) : []
+ ]);
+ }
+
+ /**
+ * Update a collection item
+ *
+ * @return void
+ */
+ public function collection_edit()
+ {
+ $data = $this->request->post->get();
+ if ( ! array_key_exists('hummingbird_id', $data))
+ {
+ $this->redirect("collection/view", 303, "anime");
+ }
+
+ $this->collection_model->update($data);
+
+ $this->redirect("collection/view", 303, "anime");
+ }
+
+ /**
+ * Add a collection item
+ *
+ * @return void
+ */
+ public function collection_add()
+ {
+ $data = $this->request->post->get();
+ if ( ! array_key_exists('id', $data))
+ {
+ $this->redirect("collection/view", 303, "anime");
+ }
+
+ $this->collection_model->add($data);
+
+ $this->redirect("collection/view", 303, "anime");
+ }
+
+ /**
+ * Update an anime item
+ *
+ * @return bool
+ */
+ public function update()
+ {
+ $this->outputJSON($this->model->update($this->request->post->get()));
+ }
+}
+// End of AnimeController.php
diff --git a/src/Controller/Collection.php b/src/Controller/Collection.php
new file mode 100644
index 00000000..2349e1df
--- /dev/null
+++ b/src/Controller/Collection.php
@@ -0,0 +1,154 @@
+ '/anime/watching{/view}',
+ 'Plan to Watch' => '/anime/plan_to_watch{/view}',
+ 'On Hold' => '/anime/on_hold{/view}',
+ 'Dropped' => '/anime/dropped{/view}',
+ 'Completed' => '/anime/completed{/view}',
+ 'Collection' => '/collection/view{/view}',
+ 'All' => '/anime/all{/view}'
+ ];
+
+ /**
+ * Constructor
+ */
+ public function __construct(Container $container)
+ {
+ parent::__construct($container);
+
+ if ($this->config->show_anime_collection === FALSE)
+ {
+ unset($this->nav_routes['Collection']);
+ }
+
+ $this->collection_model = new AnimeCollectionModel($this->config);
+ $this->base_data = [
+ 'message' => '',
+ 'url_type' => 'anime',
+ 'other_type' => 'manga',
+ 'nav_routes' => $this->nav_routes,
+ 'config' => $this->config,
+ ];
+ }
+
+ /**
+ * Search for anime
+ *
+ * @return void
+ */
+ public function search()
+ {
+ $query = $this->request->query->get('query');
+ $this->outputJSON($this->model->search($query));
+ }
+
+ /**
+ * Show the anime collection page
+ *
+ * @return void
+ */
+ public function index($view)
+ {
+ $view_map = [
+ '' => 'cover',
+ 'list' => 'list'
+ ];
+
+ $data = $this->collection_model->get_collection();
+
+ $this->outputHTML('collection/' . $view_map[$view], [
+ 'title' => WHOSE . " Anime Collection",
+ 'sections' => $data,
+ 'genres' => $this->collection_model->get_genre_list()
+ ]);
+ }
+
+ /**
+ * Show the anime collection add/edit form
+ *
+ * @param int $id
+ * @return void
+ */
+ public function form($id=NULL)
+ {
+ $action = (is_null($id)) ? "Add" : "Edit";
+
+ $this->outputHTML('collection/'. strtolower($action), [
+ 'action' => $action,
+ 'action_url' => $this->config->full_url("collection/" . strtolower($action)),
+ 'title' => WHOSE . " Anime Collection · {$action}",
+ 'media_items' => $this->collection_model->get_media_type_list(),
+ 'item' => ($action === "Edit") ? $this->collection_model->get($id) : []
+ ]);
+ }
+
+ /**
+ * Update a collection item
+ *
+ * @return void
+ */
+ public function edit()
+ {
+ $data = $this->request->post->get();
+ if ( ! array_key_exists('hummingbird_id', $data))
+ {
+ $this->redirect("collection/view", 303, "anime");
+ }
+
+ $this->collection_model->update($data);
+
+ $this->redirect("collection/view", 303, "anime");
+ }
+
+ /**
+ * Add a collection item
+ *
+ * @return void
+ */
+ public function add()
+ {
+ $data = $this->request->post->get();
+ if ( ! array_key_exists('id', $data))
+ {
+ $this->redirect("collection/view", 303, "anime");
+ }
+
+ $this->collection_model->add($data);
+
+ $this->redirect("collection/view", 303, "anime");
+ }
+}
+// End of CollectionController.php
\ No newline at end of file
diff --git a/src/Controller/Manga.php b/src/Controller/Manga.php
new file mode 100644
index 00000000..b669cdac
--- /dev/null
+++ b/src/Controller/Manga.php
@@ -0,0 +1,94 @@
+ '/manga/reading{/view}',
+ 'Plan to Read' => '/manga/plan_to_read{/view}',
+ 'On Hold' => '/manga/on_hold{/view}',
+ 'Dropped' => '/manga/dropped{/view}',
+ 'Completed' => '/manga/completed{/view}',
+ 'All' => '/manga/all{/view}'
+ ];
+
+ /**
+ * Constructor
+ */
+ public function __construct(Container $container)
+ {
+ parent::__construct($container);
+ $config = $container->get('config');
+ $this->model = new MangaModel($container);
+ $this->base_data = [
+ 'config' => $this->config,
+ 'url_type' => 'manga',
+ 'other_type' => 'anime',
+ 'nav_routes' => $this->nav_routes
+ ];
+ }
+
+ /**
+ * Update an anime item
+ *
+ * @return bool
+ */
+ public function update()
+ {
+ $this->outputJSON($this->model->update($this->request->post->get()));
+ }
+
+ /**
+ * Get a section of the manga list
+ *
+ * @param string $status
+ * @param string $title
+ * @param string $view
+ * @return void
+ */
+ public function manga_list($status, $title, $view)
+ {
+ $view_map = [
+ '' => 'cover',
+ 'list' => 'list'
+ ];
+
+ $data = ($status !== 'all')
+ ? [$status => $this->model->get_list($status)]
+ : $this->model->get_all_lists();
+
+ $this->outputHTML('manga/' . $view_map[$view], [
+ 'title' => $title,
+ 'sections' => $data
+ ]);
+ }
+}
+// End of MangaController.php
diff --git a/src/Controller/Stats.php b/src/Controller/Stats.php
new file mode 100644
index 00000000..5bf95e06
--- /dev/null
+++ b/src/Controller/Stats.php
@@ -0,0 +1,11 @@
+client->post("libraries/{$data['id']}", [
+ 'body' => $data
+ ]);
+
+ return $result->json();
+ }
+
+ /**
+ * Get the full set of anime lists
+ *
+ * @return array
+ */
+ public function get_all_lists()
+ {
+ $output = [
+ 'Watching' => [],
+ 'Plan to Watch' => [],
+ 'On Hold' => [],
+ 'Dropped' => [],
+ 'Completed' => [],
+ ];
+
+ $data = $this->_get_list();
+
+ foreach($data as $datum)
+ {
+ switch($datum['status'])
+ {
+ case "completed":
+ $output['Completed'][] = $datum;
+ break;
+
+ case "plan-to-watch":
+ $output['Plan to Watch'][] = $datum;
+ break;
+
+ case "dropped":
+ $output['Dropped'][] = $datum;
+ break;
+
+ case "on-hold":
+ $output['On Hold'][] = $datum;
+ break;
+
+ case "currently-watching":
+ $output['Watching'][] = $datum;
+ break;
+ }
+ }
+
+ // Sort anime by name
+ foreach($output as &$status_list)
+ {
+ $this->sort_by_name($status_list);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Get a category out of the full list
+ *
+ * @param string $status
+ * @return array
+ */
+ public function get_list($status)
+ {
+ $map = [
+ 'currently-watching' => 'Watching',
+ 'plan-to-watch' => 'Plan to Watch',
+ 'on-hold' => 'On Hold',
+ 'dropped' => 'Dropped',
+ 'completed' => 'Completed',
+ ];
+
+ $data = $this->_get_list($status);
+ $this->sort_by_name($data);
+
+ $output = [];
+ $output[$map[$status]] = $data;
+
+ return $output;
+ }
+
+ /**
+ * Get information about an anime from its id
+ *
+ * @param string $anime_id
+ * @return array
+ */
+ public function get_anime($anime_id)
+ {
+ $config = [
+ 'query' => [
+ 'id' => $anime_id
+ ]
+ ];
+
+ $response = $this->client->get("anime/{$anime_id}", $config);
+
+ return $response->json();
+ }
+
+ /**
+ * Search for anime by name
+ *
+ * @param string $name
+ * @return array
+ */
+ public function search($name)
+ {
+ global $defaultHandler;
+
+ $config = [
+ 'query' => [
+ 'query' => $name
+ ]
+ ];
+
+ $response = $this->client->get('search/anime', $config);
+ $defaultHandler->addDataTable('anime_search_response', (array)$response);
+
+ if ($response->getStatusCode() != 200)
+ {
+ throw new RuntimeException($response->getEffectiveUrl());
+ }
+
+ return $response->json();
+ }
+
+ /**
+ * Actually retreive the data from the api
+ *
+ * @param string $status - Status to filter by
+ * @return array
+ */
+ private function _get_list($status="all")
+ {
+ global $defaultHandler;
+
+ $cache_file = "{$this->config->data_cache_path}/anime-{$status}.json";
+
+ $config = [
+ 'allow_redirects' => FALSE
+ ];
+
+ if ($status != "all")
+ {
+ $config['query']['status'] = $status;
+ }
+
+ $response = $this->client->get("users/{$this->config->hummingbird_username}/library", $config);
+
+ $defaultHandler->addDataTable('anime_list_response', (array)$response);
+
+ if ($response->getStatusCode() != 200)
+ {
+ if ( ! file_exists($cache_file))
+ {
+ throw new DomainException($response->getEffectiveUrl());
+ }
+ else
+ {
+ $output = json_decode(file_get_contents($cache_file), TRUE);
+ }
+ }
+ else
+ {
+ $output = $response->json();
+ $output_json = json_encode($output);
+
+ if (( ! file_exists($cache_file)) || file_get_contents($cache_file) !== $output_json)
+ {
+ // Attempt to create the cache folder if it doesn't exist
+ if ( ! is_dir($this->config->data_cache_path))
+ {
+ mkdir($this->config->data_cache_path);
+ }
+ // Cache the call in case of downtime
+ file_put_contents($cache_file, json_encode($output));
+ }
+ }
+
+ foreach($output as &$row)
+ {
+ $row['anime']['cover_image'] = $this->get_cached_image($row['anime']['cover_image'], $row['anime']['slug'], 'anime');
+ }
+
+ return $output;
+ }
+
+ /**
+ * Sort the list by title
+ *
+ * @param array $array
+ * @return void
+ */
+ private function sort_by_name(&$array)
+ {
+ $sort = array();
+
+ foreach($array as $key => $item)
+ {
+ $sort[$key] = $item['anime']['title'];
+ }
+
+ array_multisort($sort, SORT_ASC, $array);
+ }
+}
+// End of AnimeModel.php
\ No newline at end of file
diff --git a/src/Model/AnimeCollection.php b/src/Model/AnimeCollection.php
new file mode 100644
index 00000000..a550f420
--- /dev/null
+++ b/src/Model/AnimeCollection.php
@@ -0,0 +1,379 @@
+db = \Query($this->db_config['collection']);
+ $this->anime_model = new AnimeModel($container);
+
+ // Is database valid? If not, set a flag so the
+ // app can be run without a valid database
+ $db_file_name = $this->db_config['collection']['file'];
+ if ($db_file_name !== ':memory:')
+ {
+ $db_file = file_get_contents($db_file_name);
+ $this->valid_database = (strpos($db_file, 'SQLite format 3') === 0);
+ }
+ else
+ {
+ $this->valid_database = TRUE;
+ }
+
+ // Do an import if an import file exists
+ $this->json_import();
+ }
+
+ /**
+ * Get genres for anime collection items
+ *
+ * @param array $filter
+ * @return array
+ */
+ public function get_genre_list($filter=[])
+ {
+ $this->db->select('hummingbird_id, genre')
+ ->from('genre_anime_set_link gl')
+ ->join('genres g', 'g.id=gl.genre_id', 'left');
+
+
+ if ( ! empty($filter)) $this->db->where_in('hummingbird_id', $filter);
+
+ $query = $this->db->order_by('hummingbird_id')
+ ->order_by('genre')
+ ->get();
+
+ $output = [];
+
+ foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $row)
+ {
+ $id = $row['hummingbird_id'];
+ $genre = $row['genre'];
+
+ // Empty genre names aren't useful
+ if (empty($genre)) continue;
+
+
+ if (array_key_exists($id, $output))
+ {
+ array_push($output[$id], $genre);
+ }
+ else
+ {
+ $output[$id] = [$genre];
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Get collection from the database, and organize by media type
+ *
+ * @return array
+ */
+ public function get_collection()
+ {
+ $raw_collection = $this->_get_collection();
+
+ $collection = [];
+
+ foreach($raw_collection as $row)
+ {
+ if (array_key_exists($row['media'], $collection))
+ {
+ $collection[$row['media']][] = $row;
+ }
+ else
+ {
+ $collection[$row['media']] = [$row];
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Get list of media types
+ *
+ * @return array
+ */
+ public function get_media_type_list()
+ {
+ $output = array();
+
+ $query = $this->db->select('id, type')
+ ->from('media')
+ ->get();
+
+ foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $row)
+ {
+ $output[$row['id']] = $row['type'];
+ }
+
+ return $output;
+ }
+
+ /**
+ * Get item from collection for editing
+ *
+ * @param int $id
+ * @return array
+ */
+ public function get_collection_entry($id)
+ {
+ $query = $this->db->from('anime_set')
+ ->where('hummingbird_id', (int) $id)
+ ->get();
+
+ return $query->fetch(\PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Get full collection from the database
+ *
+ * @return array
+ */
+ private function _get_collection()
+ {
+ if ( ! $this->valid_database) return [];
+
+ $query = $this->db->select('hummingbird_id, slug, title, alternate_title, show_type, age_rating, episode_count, episode_length, cover_image, notes, media.type as media')
+ ->from('anime_set a')
+ ->join('media', 'media.id=a.media_id', 'inner')
+ ->order_by('media')
+ ->order_by('title')
+ ->get();
+
+ return $query->fetchAll(\PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Add an item to the anime collection
+ *
+ * @param array $data
+ * @return void
+ */
+ public function add($data)
+ {
+ $anime = (object) $this->anime_model->get_anime($data['id']);
+
+ $this->db->set([
+ 'hummingbird_id' => $data['id'],
+ 'slug' => $anime->slug,
+ 'title' => $anime->title,
+ 'alternate_title' => $anime->alternate_title,
+ 'show_type' => $anime->show_type,
+ 'age_rating' => $anime->age_rating,
+ 'cover_image' => basename($this->get_cached_image($anime->cover_image, $anime->slug, 'anime')),
+ 'episode_count' => $anime->episode_count,
+ 'episode_length' => $anime->episode_length,
+ 'media_id' => $data['media_id'],
+ 'notes' => $data['notes']
+ ])->insert('anime_set');
+
+ $this->update_genre($data['id']);
+ }
+
+ /**
+ * Update a collection item
+ *
+ * @param array $data
+ * @return void
+ */
+ public function update($data)
+ {
+ // If there's no id to update, don't update
+ if ( ! array_key_exists('hummingbird_id', $data)) return;
+
+ $id = $data['hummingbird_id'];
+ unset($data['hummingbird_id']);
+
+ $this->db->set($data)
+ ->where('hummingbird_id', $id)
+ ->update('anime_set');
+ }
+
+ /**
+ * Get the details of a collection item
+ *
+ * @param int $hummingbird_id
+ * @return array
+ */
+ public function get($hummingbird_id)
+ {
+ $query = $this->db->from('anime_set')
+ ->where('hummingbird_id', $hummingbird_id)
+ ->get();
+
+ return $query->fetch(\PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Import anime into collection from a json file
+ *
+ * @return void
+ */
+ private function json_import()
+ {
+ if ( ! file_exists('import.json')) return;
+ if ( ! $this->valid_database) return;
+
+ $anime = json_decode(file_get_contents("import.json"));
+
+ foreach($anime as $item)
+ {
+ $this->db->set([
+ 'hummingbird_id' => $item->id,
+ 'slug' => $item->slug,
+ 'title' => $item->title,
+ 'alternate_title' => $item->alternate_title,
+ 'show_type' => $item->show_type,
+ 'age_rating' => $item->age_rating,
+ 'cover_image' => basename($this->get_cached_image($item->cover_image, $item->slug, 'anime')),
+ 'episode_count' => $item->episode_count,
+ 'episode_length' => $item->episode_length
+ ])->insert('anime_set');
+ }
+
+ // Delete the import file
+ unlink('import.json');
+
+ // Update genre info
+ $this->update_genres();
+ }
+
+ /**
+ * Update genre information for selected anime
+ *
+ * @return void
+ */
+ private function update_genre($anime_id)
+ {
+ $genre_info = $this->get_genre_data();
+ extract($genre_info);
+
+ // Get api information
+ $anime = $this->anime_model->get_anime($anime_id);
+
+ foreach($anime['genres'] as $genre)
+ {
+ // Add genres that don't currently exist
+ if ( ! in_array($genre['name'], $genres))
+ {
+ $this->db->set('genre', $genre['name'])
+ ->insert('genres');
+
+ $genres[] = $genre['name'];
+ }
+
+ // Update link table
+ // Get id of genre to put in link table
+ $flipped_genres = array_flip($genres);
+
+ $insert_array = [
+ 'hummingbird_id' => $anime['id'],
+ 'genre_id' => $flipped_genres[$genre['name']]
+ ];
+
+ if (array_key_exists($anime['id'], $links))
+ {
+ if ( ! in_array($flipped_genres[$genre['name']], $links[$anime['id']]))
+ {
+ $this->db->set($insert_array)->insert('genre_anime_set_link');
+ }
+ }
+ else
+ {
+ $this->db->set($insert_array)->insert('genre_anime_set_link');
+ }
+ }
+ }
+
+ /**
+ * Get list of existing genres
+ *
+ * @return array
+ */
+ private function get_genre_data()
+ {
+ $genres = [];
+ $links = [];
+
+ // Get existing genres
+ $query = $this->db->select('id, genre')
+ ->from('genres')
+ ->get();
+ foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $genre)
+ {
+ $genres[$genre['id']] = $genre['genre'];
+ }
+
+ // Get existing link table entries
+ $query = $this->db->select('hummingbird_id, genre_id')
+ ->from('genre_anime_set_link')
+ ->get();
+ foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $link)
+ {
+ if (array_key_exists($link['hummingbird_id'], $links))
+ {
+ $links[$link['hummingbird_id']][] = $link['genre_id'];
+ }
+ else
+ {
+ $links[$link['hummingbird_id']] = [$link['genre_id']];
+ }
+ }
+
+ return [
+ 'genres' => $genres,
+ 'links' => $links
+ ];
+ }
+
+ /**
+ * Update genre information for the entire collection
+ *
+ * @return void
+ */
+ private function update_genres()
+ {
+ // Get the anime collection
+ $collection = $this->_get_collection();
+ foreach($collection as $anime)
+ {
+ // Get api information
+ $this->update_genre($anime['hummingbird_id']);
+ }
+ }
+}
+// End of AnimeCollectionModel.php
\ No newline at end of file
diff --git a/src/Model/Manga.php b/src/Model/Manga.php
new file mode 100644
index 00000000..7e6104ee
--- /dev/null
+++ b/src/Model/Manga.php
@@ -0,0 +1,192 @@
+client->put("manga_library_entries/{$id}", [
+ 'cookies' => ['token' => $_SESSION['hummingbird_anime_token']],
+ 'json' => ['manga_library_entry' => $data]
+ ]);
+
+ return $result->json();
+ }
+
+ /**
+ * Get the full set of anime lists
+ *
+ * @return array
+ */
+ public function get_all_lists()
+ {
+ $data = $this->_get_list();
+
+ foreach ($data as $key => &$val)
+ {
+ $this->sort_by_name($val);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get a category out of the full list
+ *
+ * @param string $status
+ * @return array
+ */
+ public function get_list($status)
+ {
+ $data = $this->_get_list($status);
+
+ $this->sort_by_name($data);
+
+ return $data;
+ }
+
+ /**
+ * Massage the list of manga entries into something more usable
+ *
+ * @param string $status
+ * @return array
+ */
+ private function _get_list($status="all")
+ {
+ global $defaultHandler;
+
+ $cache_file = _dir($this->config->data_cache_path, 'manga.json');
+
+ $config = [
+ 'query' => [
+ 'user_id' => $this->config->hummingbird_username
+ ],
+ 'allow_redirects' => FALSE
+ ];
+
+ $response = $this->client->get('manga_library_entries', $config);
+
+ $defaultHandler->addDataTable('response', (array)$response);
+
+ if ($response->getStatusCode() != 200)
+ {
+ if ( ! file_exists($cache_file))
+ {
+ throw new DomainException($response->getEffectiveUrl());
+ }
+ else
+ {
+ $raw_data = json_decode(file_get_contents($cache_file), TRUE);
+ }
+ }
+ else
+ {
+ // Reorganize data to be more usable
+ $raw_data = $response->json();
+
+ // Attempt to create the cache dir if it doesn't exist
+ if ( ! is_dir($this->config->data_cache_path))
+ {
+ mkdir($this->config->data_cache_path);
+ }
+
+ // Cache data in case of downtime
+ file_put_contents($cache_file, json_encode($raw_data));
+ }
+
+ // Bail out early if there isn't any manga data
+ if ( ! array_key_exists('manga', $raw_data)) return [];
+
+ $data = [
+ 'Reading' => [],
+ 'Plan to Read' => [],
+ 'On Hold' => [],
+ 'Dropped' => [],
+ 'Completed' => [],
+ ];
+ $manga_data = [];
+
+ // Massage the two lists into one
+ foreach($raw_data['manga'] as $manga)
+ {
+ $manga_data[$manga['id']] = $manga;
+ }
+
+ // Filter data by status
+ foreach($raw_data['manga_library_entries'] as &$entry)
+ {
+ $entry['manga'] = $manga_data[$entry['manga_id']];
+
+ // Cache poster images
+ $entry['manga']['poster_image'] = $this->get_cached_image($entry['manga']['poster_image'], $entry['manga_id'], 'manga');
+
+ switch($entry['status'])
+ {
+ case "Plan to Read":
+ $data['Plan to Read'][] = $entry;
+ break;
+
+ case "Dropped":
+ $data['Dropped'][] = $entry;
+ break;
+
+ case "On Hold":
+ $data['On Hold'][] = $entry;
+ break;
+
+ case "Currently Reading":
+ $data['Reading'][] = $entry;
+ break;
+
+ case "Completed":
+ default:
+ $data['Completed'][] = $entry;
+ break;
+ }
+ }
+
+ return (array_key_exists($status, $data)) ? $data[$status] : $data;
+ }
+
+ /**
+ * Sort the manga entries by their title
+ *
+ * @param array $array
+ * @return void
+ */
+ private function sort_by_name(&$array)
+ {
+ $sort = array();
+
+ foreach($array as $key => $item)
+ {
+ $sort[$key] = $item['manga']['romaji_title'];
+ }
+
+ array_multisort($sort, SORT_ASC, $array);
+ }
+}
+// End of MangaModel.php
\ No newline at end of file
diff --git a/src/Model/Stats.php b/src/Model/Stats.php
new file mode 100644
index 00000000..6cf73b42
--- /dev/null
+++ b/src/Model/Stats.php
@@ -0,0 +1,27 @@
+chartSetup();
+ }
+
+}
+// End of Stats.php
\ No newline at end of file
diff --git a/src/Model/StatsChartsTrait.php b/src/Model/StatsChartsTrait.php
new file mode 100644
index 00000000..59babbae
--- /dev/null
+++ b/src/Model/StatsChartsTrait.php
@@ -0,0 +1,29 @@
+pchart = new pChartFactory();
+ }
+
+}
+// End of StatsChartsTrait.php
\ No newline at end of file
diff --git a/src/functions.php b/src/functions.php
new file mode 100644
index 00000000..fa9d81ce
--- /dev/null
+++ b/src/functions.php
@@ -0,0 +1,68 @@
+ list">
- ">
+
= WHOSE ?> = ucfirst($url_type) ?> = (strpos($route_path, 'collection') !== FALSE) ? 'Collection' : 'List' ?>
- [">= ucfirst($other_type) ?> List]
+ [= ucfirst($other_type) ?> List]
@@ -28,14 +28,14 @@
diff --git a/app/views/login.php b/src/views/login.php
similarity index 100%
rename from app/views/login.php
rename to src/views/login.php
diff --git a/app/views/manga/cover.php b/src/views/manga/cover.php
similarity index 100%
rename from app/views/manga/cover.php
rename to src/views/manga/cover.php
diff --git a/app/views/manga/list.php b/src/views/manga/list.php
similarity index 100%
rename from app/views/manga/list.php
rename to src/views/manga/list.php
diff --git a/app/views/message.php b/src/views/message.php
similarity index 100%
rename from app/views/message.php
rename to src/views/message.php
diff --git a/tests/Base/BaseControllerTest.php b/tests/Base/BaseControllerTest.php
new file mode 100644
index 00000000..261d3c73
--- /dev/null
+++ b/tests/Base/BaseControllerTest.php
@@ -0,0 +1,61 @@
+ [],
+ '_POST' => [],
+ '_COOKIE' => [],
+ '_SERVER' => $_SERVER,
+ '_FILES' => []
+ ]);
+ $this->container->set('request', $web_factory->newRequest());
+ $this->container->set('response', $web_factory->newResponse());
+
+ $this->BaseController = new Controller($this->container);
+ }
+
+ public function testBaseControllerSanity()
+ {
+ $this->assertTrue(is_object($this->BaseController));
+ }
+
+ public function dataGet()
+ {
+ return [
+ 'request' => [
+ 'key' => 'request',
+ ],
+ 'response' => [
+ 'key' => 'response',
+ ],
+ 'config' => [
+ 'key' => 'config',
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider dataGet
+ */
+ public function testGet($key)
+ {
+ $result = $this->BaseController->__get($key);
+ $this->assertEquals($this->container->get($key), $result);
+ }
+
+ public function testGetNull()
+ {
+ $result = $this->BaseController->__get('foo');
+ $this->assertNull($result);
+ }
+
+}
\ No newline at end of file
diff --git a/tests/base/BaseModelTest.php b/tests/Base/BaseModelTest.php
similarity index 55%
rename from tests/base/BaseModelTest.php
rename to tests/Base/BaseModelTest.php
index df0dcd66..07388f2d 100644
--- a/tests/base/BaseModelTest.php
+++ b/tests/Base/BaseModelTest.php
@@ -1,12 +1,13 @@
config);
+ $baseModel = new BaseModel($this->container);
$this->assertTrue(is_object($baseModel));
}
}
\ No newline at end of file
diff --git a/tests/base/ConfigTest.php b/tests/Base/ConfigTest.php
similarity index 50%
rename from tests/base/ConfigTest.php
rename to tests/Base/ConfigTest.php
index 8ce2d3e8..2a46d654 100644
--- a/tests/base/ConfigTest.php
+++ b/tests/Base/ConfigTest.php
@@ -1,19 +1,15 @@
config = new Config([
- 'config' => [
- 'foo' => 'bar',
- 'asset_path' => '//localhost/assets/'
- ],
- 'base_config' => [
- 'bar' => 'baz'
- ]
+ 'foo' => 'bar',
+ 'asset_path' => '//localhost/assets/',
+ 'bar' => 'baz'
]);
}
@@ -57,20 +53,19 @@ class ConfigTest extends AnimeClient_TestCase {
$this->assertEquals($expected, $result);
}
- public function fullUrlProvider()
+ public function dataFullUrl()
{
return [
'default_view' => [
'config' => [
- 'anime_host' => '',
- 'manga_host' => '',
- 'anime_path' => 'anime',
- 'manga_path' => 'manga',
- 'route_by' => 'host',
- 'default_list' => 'manga',
- 'default_anime_path' => '/watching',
- 'default_manga_path' => '/all',
- 'default_to_list_view' => FALSE,
+ 'routing' => [
+ 'anime_path' => 'anime',
+ 'manga_path' => 'manga',
+ 'default_list' => 'manga',
+ 'default_anime_path' => '/anime/watching',
+ 'default_manga_path' => '/manga/all',
+ 'default_to_list_view' => FALSE,
+ ],
],
'path' => '',
'type' => 'manga',
@@ -78,15 +73,14 @@ class ConfigTest extends AnimeClient_TestCase {
],
'default_view_list' => [
'config' => [
- 'anime_host' => '',
- 'manga_host' => '',
- 'anime_path' => 'anime',
- 'manga_path' => 'manga',
- 'route_by' => 'host',
- 'default_list' => 'manga',
- 'default_anime_path' => '/watching',
- 'default_manga_path' => '/all',
- 'default_to_list_view' => TRUE,
+ 'routing' => [
+ 'anime_path' => 'anime',
+ 'manga_path' => 'manga',
+ 'default_list' => 'manga',
+ 'default_anime_path' => '/anime/watching',
+ 'default_manga_path' => '/manga/all',
+ 'default_to_list_view' => TRUE,
+ ],
],
'path' => '',
'type' => 'manga',
@@ -96,14 +90,52 @@ class ConfigTest extends AnimeClient_TestCase {
}
/**
- * @dataProvider fullUrlProvider
+ * @dataProvider dataFullUrl
*/
public function testFullUrl($config, $path, $type, $expected)
{
- $this->config = new Config(['config' => $config, 'base_config' => []]);
+ $this->config = new Config($config);
$result = $this->config->full_url($path, $type);
$this->assertEquals($expected, $result);
}
+
+ public function dataBaseUrl()
+ {
+ $config = [
+ 'routing' => [
+ 'anime_path' => 'anime',
+ 'manga_path' => 'manga',
+ 'default_list' => 'manga',
+ 'default_anime_path' => '/watching',
+ 'default_manga_path' => '/all',
+ 'default_to_list_view' => TRUE,
+ ],
+ ];
+
+ return [
+ 'path_based_routing_anime' => [
+ 'config' => $config,
+ 'type' => 'anime',
+ 'expected' => '//localhost/anime'
+ ],
+ 'path_based_routing_manga' => [
+ 'config' => $config,
+ 'type' => 'manga',
+ 'expected' => '//localhost/manga'
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider dataBaseUrl
+ */
+ public function testBaseUrl($config, $type, $expected)
+ {
+ $this->config = new Config($config);
+ $result = $this->config->base_url($type);
+
+ $this->assertEquals($expected, $result);
+ }
}
\ No newline at end of file
diff --git a/tests/base/CoreTest.php b/tests/Base/CoreTest.php
similarity index 100%
rename from tests/base/CoreTest.php
rename to tests/Base/CoreTest.php
diff --git a/tests/base/BaseApiModelTest.php b/tests/Base/Model/BaseApiModelTest.php
similarity index 59%
rename from tests/base/BaseApiModelTest.php
rename to tests/Base/Model/BaseApiModelTest.php
index da3f6052..8785f6ca 100644
--- a/tests/base/BaseApiModelTest.php
+++ b/tests/Base/Model/BaseApiModelTest.php
@@ -1,12 +1,13 @@
config);
+ $baseApiModel = new MockBaseApiModel($this->container);
// Some basic type checks for class memebers
- $this->assertInstanceOf('\AnimeClient\BaseModel', $baseApiModel);
- $this->assertInstanceOf('\AnimeClient\BaseApiModel', $baseApiModel);
+ $this->assertInstanceOf('\AnimeClient\Base\Model', $baseApiModel);
+ $this->assertInstanceOf('\AnimeClient\Base\Model\API', $baseApiModel);
$this->assertInstanceOf('\GuzzleHttp\Client', $baseApiModel->client);
$this->assertInstanceOf('\GuzzleHttp\Cookie\CookieJar', $baseApiModel->cookieJar);
diff --git a/tests/base/BaseDBModelTest.php b/tests/Base/Model/BaseDBModelTest.php
similarity index 61%
rename from tests/base/BaseDBModelTest.php
rename to tests/Base/Model/BaseDBModelTest.php
index 896d89c7..1b1f37ef 100644
--- a/tests/base/BaseDBModelTest.php
+++ b/tests/Base/Model/BaseDBModelTest.php
@@ -1,12 +1,12 @@
config);
+ $baseDBModel = new BaseDBModel($this->container);
$this->assertTrue(is_object($baseDBModel));
}
}
\ No newline at end of file
diff --git a/tests/Base/RouterTest.php b/tests/Base/RouterTest.php
new file mode 100644
index 00000000..026d4b60
--- /dev/null
+++ b/tests/Base/RouterTest.php
@@ -0,0 +1,193 @@
+ 'GET',
+ 'REQUEST_URI' => $uri,
+ 'PATH_INFO' => $uri,
+ 'HTTP_HOST' => $host,
+ 'SERVER_NAME' => $host
+ ]);
+
+ $router_factory = new RouterFactory();
+ $web_factory = new WebFactory([
+ '_GET' => [],
+ '_POST' => [],
+ '_COOKIE' => [],
+ '_SERVER' => $_SERVER,
+ '_FILES' => []
+ ]);
+
+ // Add the appropriate objects to the container
+ $this->container = new Container([
+ 'config' => new Config($config),
+ 'request' => $web_factory->newRequest(),
+ 'response' => $web_factory->newResponse(),
+ 'aura-router' => $router_factory->newInstance(),
+ 'error-handler' => new MockErrorHandler()
+ ]);
+
+ $this->router = new Router($this->container);
+ $this->config = $this->container->get('config');
+ }
+
+ public function testRouterSanity()
+ {
+ $this->_set_up([], '/', 'localhost');
+ $this->assertTrue(is_object($this->router));
+ }
+
+ public function dataRoute()
+ {
+ $default_config = array(
+ 'routing' => [
+ 'anime_path' => 'anime',
+ 'manga_path' => 'manga',
+ 'default_list' => 'anime'
+ ]
+ );
+
+ $data = [
+ 'manga_path_routing' => array(
+ 'config' => $default_config,
+ 'type' => 'manga',
+ 'host' => "localhost",
+ 'uri' => "/manga/plan_to_read",
+ ),
+ 'anime_path_routing' => array(
+ 'config' => $default_config,
+ 'type' => 'anime',
+ 'host' => "localhost",
+ 'uri' => "/anime/watching",
+ )
+ ];
+
+ $data['anime_path_routing']['config']['routing']['default_list'] = 'manga';
+
+ return $data;
+ }
+
+ /**
+ * @dataProvider dataRoute
+ */
+ public function testRoute($config, $type, $host, $uri)
+ {
+ $check_var = "{$type}_path";
+ $config['base_config']['routes'] = [
+ 'common' => [
+ 'login_form' => [
+ 'path' => '/login',
+ 'action' => ['login'],
+ 'verb' => 'get'
+ ],
+ ],
+ 'anime' => [
+ 'watching' => [
+ 'path' => '/anime/watching{/view}',
+ 'action' => ['anime_list'],
+ 'params' => [
+ 'type' => 'currently-watching',
+ 'title' => WHOSE . " Anime List · Watching"
+ ],
+ 'tokens' => [
+ 'view' => '[a-z_]+'
+ ]
+ ],
+ ],
+ 'manga' => [
+ 'plan_to_read' => [
+ 'path' => '/manga/plan_to_read{/view}',
+ 'action' => ['manga_list'],
+ 'params' => [
+ 'type' => 'Plan to Read',
+ 'title' => WHOSE . " Manga List · Plan to Read"
+ ],
+ 'tokens' => [
+ 'view' => '[a-z_]+'
+ ]
+ ],
+ ]
+ ];
+
+ $this->_set_up($config, $uri, $host);
+
+ $request = $this->container->get('request');
+ $aura_router = $this->container->get('aura-router');
+
+ // Check route setup
+ $this->assertEquals($config['base_config']['routes'], $this->config->routes, "Incorrect route path");
+ $this->assertTrue(is_array($this->router->get_output_routes()));
+
+ // Check environment variables
+ $this->assertEquals($uri, $request->server->get('REQUEST_URI'));
+ $this->assertEquals($host, $request->server->get('HTTP_HOST'));
+
+ // Make sure the route is an anime type
+ $this->assertTrue($aura_router->count() > 0, "0 routes");
+ $this->assertTrue($this->config->$check_var !== '', "Check variable is empty");
+ $this->assertEquals($type, $this->router->get_controller(), "Incorrect Route type");
+
+ // Make sure the route matches, by checking that it is actually an object
+ $route = $this->router->get_route();
+ $this->assertInstanceOf('Aura\\Router\\Route', $route, "Route is invalid, not matched");
+ }
+
+ public function testDefaultRoute()
+ {
+ $config = [
+ 'routing' => [
+ 'anime_path' => 'anime',
+ 'manga_path' => 'manga',
+ 'default_list' => 'manga'
+ ],
+ 'routes' => [
+ 'common' => [
+ 'login_form' => [
+ 'path' => '/login',
+ 'action' => ['login'],
+ 'verb' => 'get'
+ ],
+ ],
+ 'anime' => [
+ 'index' => [
+ 'path' => '/',
+ 'action' => ['redirect'],
+ 'params' => [
+ 'url' => '', // Determined by config
+ 'code' => '301'
+ ]
+ ],
+ ],
+ 'manga' => [
+ 'index' => [
+ 'path' => '/',
+ 'action' => ['redirect'],
+ 'params' => [
+ 'url' => '', // Determined by config
+ 'code' => '301',
+ 'type' => 'manga'
+ ]
+ ],
+ ]
+ ]
+ ];
+
+ $this->_set_up($config, "/", "localhost");
+ //$this->assertEquals($this->config->full_url('', 'manga'), $this->response->headers->get('location'));
+ $this->assertEquals('//localhost/manga/', $this->config->full_url('', 'manga'), "Incorrect default url");
+ }
+}
\ No newline at end of file
diff --git a/tests/base/FunctionsTest.php b/tests/FunctionsTest.php
similarity index 100%
rename from tests/base/FunctionsTest.php
rename to tests/FunctionsTest.php
diff --git a/tests/Model/AnimeCollectionModelest.php b/tests/Model/AnimeCollectionModelest.php
new file mode 100644
index 00000000..51acbe5d
--- /dev/null
+++ b/tests/Model/AnimeCollectionModelest.php
@@ -0,0 +1,7 @@
+ [],
- '_POST' => [],
- '_COOKIE' => [],
- '_SERVER' => $_SERVER,
- '_FILES' => []
- ]);
- $request = $web_factory->newRequest();
- $response = $web_factory->newResponse();
-
- $this->BaseController = new BaseController($this->config, [$request, $response]);
- }
-
- public function testBaseControllerSanity()
- {
- $this->assertTrue(is_object($this->BaseController));
- }
-
-}
\ No newline at end of file
diff --git a/tests/base/RouterTest.php b/tests/base/RouterTest.php
deleted file mode 100644
index 72299832..00000000
--- a/tests/base/RouterTest.php
+++ /dev/null
@@ -1,250 +0,0 @@
-aura_router = $router_factory->newInstance();
-
- // Create Request/Response Objects
- $web_factory = new WebFactory([
- '_GET' => [],
- '_POST' => [],
- '_COOKIE' => [],
- '_SERVER' => $_SERVER,
- '_FILES' => []
- ]);
- $this->request = $web_factory->newRequest();
- $this->response = $web_factory->newResponse();
- $this->router = new Router($this->config, $this->aura_router, $this->request, $this->response);
-
- $this->assertTrue(is_object($this->router));
- }
-
- protected function _set_up($config, $uri, $host)
- {
- $this->config = new Config($config);
-
- // Set up the environment
- $_SERVER = array_merge($_SERVER, [
- 'REQUEST_METHOD' => 'GET',
- 'REQUEST_URI' => $uri,
- 'HTTP_HOST' => $host,
- 'SERVER_NAME' => $host
- ]);
-
- $router_factory = new RouterFactory();
- $this->aura_router = $router_factory->newInstance();
-
- // Create Request/Response Objects
- $web_factory = new WebFactory([
- '_GET' => [],
- '_POST' => [],
- '_COOKIE' => [],
- '_SERVER' => $_SERVER,
- '_FILES' => []
- ]);
- $this->request = $web_factory->newRequest();
- $this->response = $web_factory->newResponse();
- $this->router = new Router($this->config, $this->aura_router, $this->request, $this->response);
- }
-
- public function RouteTestProvider()
- {
- return [
- 'manga_path_routing' => array(
- 'config' => array(
- 'config' => [
- 'anime_host' => '',
- 'manga_host' => '',
- 'anime_path' => 'anime',
- 'manga_path' => 'manga',
- 'route_by' => 'path',
- 'default_list' => 'anime'
- ],
- 'base_config' => []
- ),
- 'type' => 'manga',
- 'host' => "localhost",
- 'uri' => "/manga/plan_to_read",
- 'check_var' => 'manga_path'
- ),
- 'manga_host_routing' => array(
- 'config' => array(
- 'config' => [
- 'anime_host' => 'anime.host.me',
- 'manga_host' => 'manga.host.me',
- 'anime_path' => '',
- 'manga_path' => '',
- 'route_by' => 'host',
- 'default_list' => 'anime'
- ],
- 'base_config' => []
- ),
- 'type' => 'manga',
- 'host' => 'manga.host.me',
- 'uri' => '/plan_to_read',
- 'check_var' => 'manga_host'
- ),
- 'anime_path_routing' => array(
- 'config' => array(
- 'config' => [
- 'anime_host' => '',
- 'manga_host' => '',
- 'anime_path' => 'anime',
- 'manga_path' => 'manga',
- 'route_by' => 'path',
- 'default_list' => 'manga'
- ],
- 'base_config' => [
- 'routes' => []
- ]
- ),
- 'type' => 'anime',
- 'host' => "localhost",
- 'uri' => "/anime/watching",
- 'check_var' => 'anime_path'
- ),
- 'anime_host_routing' => array(
- 'config' => array(
- 'config' => [
- 'anime_host' => 'anime.host.me',
- 'manga_host' => 'manga.host.me',
- 'anime_path' => '',
- 'manga_path' => '',
- 'route_by' => 'host',
- 'default_list' => 'manga'
- ],
- 'base_config' => []
- ),
- 'type' => 'anime',
- 'host' => 'anime.host.me',
- 'uri' => '/watching',
- 'check_var' => 'anime_host'
- ),
- ];
- }
-
- /**
- * @dataProvider RouteTestProvider
- */
- public function testRoute($config, $type, $host, $uri, $check_var)
- {
- $config['base_config']['routes'] = [
- 'common' => [
- 'login_form' => [
- 'path' => '/login',
- 'action' => ['login'],
- 'verb' => 'get'
- ],
- ],
- 'anime' => [
- 'watching' => [
- 'path' => '/watching{/view}',
- 'action' => ['anime_list'],
- 'params' => [
- 'type' => 'currently-watching',
- 'title' => WHOSE . " Anime List · Watching"
- ],
- 'tokens' => [
- 'view' => '[a-z_]+'
- ]
- ],
- ],
- 'manga' => [
- 'plan_to_read' => [
- 'path' => '/plan_to_read{/view}',
- 'action' => ['manga_list'],
- 'params' => [
- 'type' => 'Plan to Read',
- 'title' => WHOSE . " Manga List · Plan to Read"
- ],
- 'tokens' => [
- 'view' => '[a-z_]+'
- ]
- ],
- ]
- ];
-
- $this->_set_up($config, $uri, $host);
-
- // Check route setup
- $this->assertEquals($config['base_config']['routes'], $this->config->routes);
- $this->assertTrue(is_array($this->router->get_output_routes()));
-
- // Check environment variables
- $this->assertEquals($uri, $this->request->server->get('REQUEST_URI'));
- $this->assertEquals($host, $this->request->server->get('HTTP_HOST'));
-
- // Make sure the route is an anime type
- $this->assertTrue($this->aura_router->count() > 0, "More than 0 routes");
- $this->assertTrue($this->config->$check_var !== '', "Check variable is not empty");
- $this->assertEquals($type, $this->router->get_route_type(), "Correct Route type");
-
- // Make sure the route matches, by checking that it is actually an object
- $route = $this->router->get_route();
- $this->assertInstanceOf('Aura\\Router\\Route', $route, "Route is valid, and matched");
- }
-
- /*public function testDefaultRoute()
- {
- $config = [
- 'config' => [
- 'anime_host' => '',
- 'manga_host' => '',
- 'anime_path' => 'anime',
- 'manga_path' => 'manga',
- 'route_by' => 'host',
- 'default_list' => 'manga'
- ],
- 'base_config' => [
- 'routes' => [
- 'common' => [
- 'login_form' => [
- 'path' => '/login',
- 'action' => ['login'],
- 'verb' => 'get'
- ],
- ],
- 'anime' => [
- 'index' => [
- 'path' => '/',
- 'action' => ['redirect'],
- 'params' => [
- 'url' => '', // Determined by config
- 'code' => '301'
- ]
- ],
- ],
- 'manga' => [
- 'index' => [
- 'path' => '/',
- 'action' => ['redirect'],
- 'params' => [
- 'url' => '', // Determined by config
- 'code' => '301',
- 'type' => 'manga'
- ]
- ],
- ]
- ]
- ]
- ];
-
- $this->_set_up($config, "/", "localhost");
- $this->assertEquals($this->config->full_url('', 'manga'), $this->response->headers->get('location'));
- }*/
-}
\ No newline at end of file
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index cc8e11a7..e9e459d9 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -3,7 +3,8 @@
* Global setup for unit tests
*/
-use \AnimeClient\Config;
+use AnimeClient\Base\Config;
+use AnimeClient\Base\Container;
// -----------------------------------------------------------------------------
// Mock the default error handler
@@ -23,28 +24,27 @@ $defaultHandler = new MockErrorHandler();
* Base class for TestCases
*/
class AnimeClient_TestCase extends PHPUnit_Framework_TestCase {
-
- protected $config;
+ protected $container;
public function setUp()
{
parent::setUp();
- global $config;
- $this->config = new Config([
- 'config' => [
- 'asset_path' => '//localhost/assets/'
- ],
- 'base_config' => [
- 'databaase' => [],
- 'routes' => [
- 'common' => [],
- 'anime' => [],
- 'manga' => []
- ]
+ $config = new Config([
+ 'asset_path' => '//localhost/assets/',
+ 'databaase' => [],
+ 'routes' => [
+ 'common' => [],
+ 'anime' => [],
+ 'manga' => []
]
]);
- $config =& $this->config;
+
+ $container = new Container([
+ 'config' => $config
+ ]);
+
+ $this->container = $container;
}
}
@@ -57,14 +57,48 @@ define('WHOSE', "Foo's");
// Define base path constants
define('ROOT_DIR', realpath(__DIR__ . DIRECTORY_SEPARATOR . "/../"));
-require ROOT_DIR . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'base' . DIRECTORY_SEPARATOR . 'pre_conf_functions.php';
+
+/**
+ * Joins paths together. Variadic to take an
+ * arbitrary number of arguments
+ *
+ * @return string
+ */
+function _dir()
+{
+ return implode(DIRECTORY_SEPARATOR, func_get_args());
+}
+
define('APP_DIR', _dir(ROOT_DIR, 'app'));
define('CONF_DIR', _dir(APP_DIR, 'config'));
-define('BASE_DIR', _dir(APP_DIR, 'base'));
+define('SRC_DIR', _dir(ROOT_DIR, 'src'));
+define('BASE_DIR', _dir(SRC_DIR, 'Base'));
+
+/**
+ * Set up autoloaders
+ *
+ * @codeCoverageIgnore
+ * @return void
+ */
+function _setup_autoloaders()
+{
+ require _dir(ROOT_DIR, '/vendor/autoload.php');
+ spl_autoload_register(function ($class) {
+ $class_parts = explode('\\', $class);
+ array_shift($class_parts);
+ $ns_path = SRC_DIR . '/' . implode('/', $class_parts) . ".php";
+
+ if (file_exists($ns_path))
+ {
+ require_once($ns_path);
+ return;
+ }
+ });
+}
// Setup autoloaders
_setup_autoloaders();
-require(_dir(BASE_DIR, 'functions.php'));
+require(_dir(SRC_DIR, 'functions.php'));
// Pre-define some superglobals
$_SESSION = [];