Some progress toward better structure through refactoring

This commit is contained in:
Timothy Warren 2015-09-14 10:54:50 -04:00
parent c5f3093a78
commit e53f9abf3f
65 changed files with 3114 additions and 471 deletions

20
.editorconfig Normal file
View File

@ -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

View File

@ -2,7 +2,7 @@
/** /**
* Base API Model * Base API Model
*/ */
namespace AnimeClient; namespace AnimeClient\Base;
use \GuzzleHttp\Client; use \GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar; use \GuzzleHttp\Cookie\CookieJar;
@ -10,7 +10,7 @@ use \GuzzleHttp\Cookie\CookieJar;
/** /**
* Base model for api interaction * Base model for api interaction
*/ */
class BaseApiModel extends BaseModel { class ApiModel extends Model {
/** /**
* Base url for making api requests * Base url for making api requests

View File

@ -1,6 +1,9 @@
<?php <?php
/**
* Base Configuration class
*/
namespace AnimeClient; namespace AnimeClient\Base;
/** /**
* Wrapper for configuration values * Wrapper for configuration values
@ -58,7 +61,7 @@ class Config {
* *
* @return string * @return string
*/ */
function asset_url(/*...*/) public function asset_url(/*...*/)
{ {
$args = func_get_args(); $args = func_get_args();
$base_url = rtrim($this->__get('asset_path'), '/'); $base_url = rtrim($this->__get('asset_path'), '/');
@ -74,7 +77,7 @@ class Config {
* @param string $type - (optional) The controller * @param string $type - (optional) The controller
* @return string * @return string
*/ */
function base_url($type="anime") public function base_url($type="anime")
{ {
$config_path = trim($this->__get("{$type}_path"), "/"); $config_path = trim($this->__get("{$type}_path"), "/");
$config_host = $this->__get("{$type}_host"); $config_host = $this->__get("{$type}_host");
@ -93,7 +96,7 @@ class Config {
* @param string $type - (optional) The controller (anime or manga), defaults to anime * @param string $type - (optional) The controller (anime or manga), defaults to anime
* @return string * @return string
*/ */
function full_url($path="", $type="anime") public function full_url($path="", $type="anime")
{ {
$config_path = trim($this->__get("{$type}_path"), "/"); $config_path = trim($this->__get("{$type}_path"), "/");
$config_host = $this->__get("{$type}_host"); $config_host = $this->__get("{$type}_host");

View File

@ -2,14 +2,14 @@
/** /**
* Base Controller * Base Controller
*/ */
namespace AnimeClient; namespace AnimeClient\Base;
use Aura\Web\WebFactory; use Aura\Web\WebFactory;
/** /**
* Base class for controllers, defines output methods * Base class for controllers, defines output methods
*/ */
class BaseController { class Controller {
/** /**
* The global configuration object * The global configuration object

View File

@ -2,12 +2,12 @@
/** /**
* Base DB model * Base DB model
*/ */
namespace AnimeClient; namespace AnimeClient\Base;
/** /**
* Base model for database interaction * Base model for database interaction
*/ */
class BaseDBModel extends BaseModel { class DBModel extends Model {
/** /**
* The query builder object * The query builder object
* @var object $db * @var object $db

View File

@ -2,14 +2,14 @@
/** /**
* Base for base models * Base for base models
*/ */
namespace AnimeClient; namespace AnimeClient\Base;
use abeautifulsite\SimpleImage; use abeautifulsite\SimpleImage;
/** /**
* Common base for all Models * Common base for all Models
*/ */
class BaseModel { class Model {
/** /**
* The global configuration object * The global configuration object

View File

@ -3,7 +3,7 @@
* Routing logic * Routing logic
*/ */
namespace AnimeClient; namespace AnimeClient\Base;
/** /**
* Basic routing/ dispatch * 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 = str_replace([$this->config->anime_path, $this->config->manga_path], '', $raw_route);
$route_path = "/" . trim($route_path, '/'); $route_path = "/" . trim($route_path, '/');
$defaultHandler->addDataTable('Route Info', [ /*$defaultHandler->addDataTable('Route Info', [
'route_path' => $route_path 'route_path' => $route_path
]); ]);*/
$route = $this->router->match($route_path, $_SERVER); $route = $this->router->match($route_path, $_SERVER);
@ -107,15 +107,6 @@ class Router {
{ {
$failure = $this->router->getFailedRoute(); $failure = $this->router->getFailedRoute();
$defaultHandler->addDataTable('failed_route', (array)$failure); $defaultHandler->addDataTable('failed_route', (array)$failure);
/*$controller_name = '\\AnimeClient\\BaseController';
$action_method = 'outputHTML';
$params = [
'template' => '404',
'data' => [
'title' => 'Page Not Found'
]
];*/
} }
else else
{ {
@ -194,8 +185,8 @@ class Router {
public function _setup_routes() public function _setup_routes()
{ {
$route_map = [ $route_map = [
'anime' => '\\AnimeClient\\AnimeController', 'anime' => '\\AnimeClient\\Controller\\Anime',
'manga' => '\\AnimeClient\\MangaController', 'manga' => '\\AnimeClient\\Controller\\Manga',
]; ];
$output_routes = []; $output_routes = [];

View File

@ -26,18 +26,13 @@ function _setup_autoloaders()
require _dir(ROOT_DIR, '/vendor/autoload.php'); require _dir(ROOT_DIR, '/vendor/autoload.php');
spl_autoload_register(function ($class) { spl_autoload_register(function ($class) {
$class_parts = explode('\\', $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"]; if (file_exists($ns_path))
foreach($dirs as $dir)
{ {
$file = _dir(APP_DIR, $dir, "{$class}.php"); require_once($ns_path);
if (file_exists($file))
{
require_once $file;
return; return;
} }
}
}); });
} }

View File

@ -3,12 +3,17 @@
* Anime Controller * 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 * Controller for Anime-related pages
*/ */
class AnimeController extends BaseController { class Anime extends BaseController {
/** /**
* The anime list model * The anime list model

View File

@ -2,12 +2,16 @@
/** /**
* Manga Controller * 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 * Controller for manga list
*/ */
class MangaController extends BaseController { class Manga extends Controller {
/** /**
* The manga model * The manga model

View File

@ -3,12 +3,15 @@
* Anime API Model * 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 * Model for handling requests dealing with the anime list
*/ */
class AnimeModel extends BaseApiModel { class Anime extends ApiModel {
/** /**
* The base url for api requests * The base url for api requests
* @var string $base_url * @var string $base_url

View File

@ -3,12 +3,16 @@
* Anime Collection DB Model * 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 * Model for getting anime collection data
*/ */
class AnimeCollectionModel extends BaseDBModel { class AnimeCollection extends DBModel {
/** /**
* Anime API Model * Anime API Model

View File

@ -2,12 +2,15 @@
/** /**
* Manga API Model * 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 * Model for handling requests dealing with the manga list
*/ */
class MangaModel extends BaseApiModel { class Manga extends ApiModel {
/** /**
* The base url for api requests * The base url for api requests

View File

@ -1,4 +1,7 @@
<?php <?php
/**
* Bootstrap / Dependency Injection
*/
namespace AnimeClient; namespace AnimeClient;
@ -6,6 +9,15 @@ use \Whoops\Handler\PrettyPageHandler;
use \Whoops\Handler\JsonResponseHandler; use \Whoops\Handler\JsonResponseHandler;
use \Aura\Web\WebFactory; use \Aura\Web\WebFactory;
use \Aura\Router\RouterFactory; use \Aura\Router\RouterFactory;
use \Aura\Di\Container as DiContainer;
use \Aura\Di\Factory as DiFactory;
require _dir(SRC_DIR, '/functions.php');
// -----------------------------------------------------------------------------
// Setup DI container
// -----------------------------------------------------------------------------
$container = new Base\Container();
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Setup error handling // Setup error handling
@ -23,17 +35,20 @@ $whoops->pushHandler($jsonHandler);
$whoops->register(); $whoops->register();
$container->set('error-handler', $defaultHandler);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Injected Objects // Injected Objects
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Create Config Object // Create Config Object
$config = new Config(); $config = new Base\Config();
require _dir(BASE_DIR, '/functions.php'); $container->set('config', $config);
// Create Aura Router Object // Create Aura Router Object
$router_factory = new RouterFactory(); $router_factory = new RouterFactory();
$aura_router = $router_factory->newInstance(); $aura_router = $router_factory->newInstance();
$container->set('aura-router', $aura_router);
// Create Request/Response Objects // Create Request/Response Objects
$web_factory = new WebFactory([ $web_factory = new WebFactory([
@ -43,13 +58,13 @@ $web_factory = new WebFactory([
'_SERVER' => $_SERVER, '_SERVER' => $_SERVER,
'_FILES' => $_FILES '_FILES' => $_FILES
]); ]);
$request = $web_factory->newRequest(); $container->set('request', $web_factory->newRequest());
$response = $web_factory->newResponse(); $container->set('response', $web_factory->newResponse());
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Router // Router
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
$router = new Router($config, $aura_router, $request, $response); $router = new Base\Router($container);
$router->dispatch(); $router->dispatch();
// End of bootstrap.php // End of bootstrap.php

View File

@ -1,4 +1,4 @@
<?php <?php
$config = [ $config = [
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Username for anime and manga lists // Username for anime and manga lists
@ -12,32 +12,50 @@ $config = [
// do you wish to show the anime collection tab? // do you wish to show the anime collection tab?
'show_anime_collection' => TRUE, 'show_anime_collection' => TRUE,
// path to public directory
'asset_path' => '//' . $_SERVER['HTTP_HOST'] . '/public',
// path to public directory on the server // path to public directory on the server
'asset_dir' => __DIR__ . '/../../public', 'asset_dir' => __DIR__ . '/../../public',
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Routing // 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 'routing' => [
'anime_host' => '', // Subfolder prefix for url
'manga_host' => '', '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', 'anime_path' => 'anime',
'manga_path' => 'manga', 'manga_path' => 'manga',
'collection_path' => 'collection',
'stats_path' => 'stats',
// Which list should be the default? // Which list should be the default?
'default_list' => 'anime', // anime or manga 'default_list' => 'anime', // anime or manga
// Default pages for anime/manga // Default pages for anime/manga
'default_anime_path' => '/watching', 'default_anime_path' => "/anime/watching",
'default_manga_path' => '/all', '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' => "/anime/watching",
'default_manga_path' => '/manga/all',
// Default to list view? // Default to list view?
'default_to_list_view' => FALSE, 'default_to_list_view' => FALSE,

View File

@ -22,6 +22,43 @@ return [
'path' => '/logout', 'path' => '/logout',
'action' => ['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 // Routes on anime controller
'anime' => [ 'anime' => [
@ -34,11 +71,11 @@ return [
] ]
], ],
'search' => [ 'search' => [
'path' => '/search', 'path' => '/anime/search',
'action' => ['search'], 'action' => ['search'],
], ],
'all' => [ 'all' => [
'path' => '/all{/view}', 'path' => '/anime/all{/view}',
'action' => ['anime_list'], 'action' => ['anime_list'],
'params' => [ 'params' => [
'type' => 'all', 'type' => 'all',
@ -49,7 +86,7 @@ return [
] ]
], ],
'watching' => [ 'watching' => [
'path' => '/watching{/view}', 'path' => '/anime/watching{/view}',
'action' => ['anime_list'], 'action' => ['anime_list'],
'params' => [ 'params' => [
'type' => 'currently-watching', 'type' => 'currently-watching',
@ -60,7 +97,7 @@ return [
] ]
], ],
'plan_to_watch' => [ 'plan_to_watch' => [
'path' => '/plan_to_watch{/view}', 'path' => '/anime/plan_to_watch{/view}',
'action' => ['anime_list'], 'action' => ['anime_list'],
'params' => [ 'params' => [
'type' => 'plan-to-watch', 'type' => 'plan-to-watch',
@ -71,7 +108,7 @@ return [
] ]
], ],
'on_hold' => [ 'on_hold' => [
'path' => '/on_hold{/view}', 'path' => '/anime/on_hold{/view}',
'action' => ['anime_list'], 'action' => ['anime_list'],
'params' => [ 'params' => [
'type' => 'on-hold', 'type' => 'on-hold',
@ -82,7 +119,7 @@ return [
] ]
], ],
'dropped' => [ 'dropped' => [
'path' => '/dropped{/view}', 'path' => '/anime/dropped{/view}',
'action' => ['anime_list'], 'action' => ['anime_list'],
'params' => [ 'params' => [
'type' => 'dropped', 'type' => 'dropped',
@ -93,7 +130,7 @@ return [
] ]
], ],
'completed' => [ 'completed' => [
'path' => '/completed{/view}', 'path' => '/anime/completed{/view}',
'action' => ['anime_list'], 'action' => ['anime_list'],
'params' => [ 'params' => [
'type' => 'completed', 'type' => 'completed',
@ -103,36 +140,6 @@ return [
'view' => '[a-z_]+' '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' => [ 'manga' => [
'index' => [ 'index' => [
@ -145,7 +152,7 @@ return [
] ]
], ],
'all' => [ 'all' => [
'path' => '/all{/view}', 'path' => '/manga/all{/view}',
'action' => ['manga_list'], 'action' => ['manga_list'],
'params' => [ 'params' => [
'type' => 'all', 'type' => 'all',
@ -156,7 +163,7 @@ return [
] ]
], ],
'reading' => [ 'reading' => [
'path' => '/reading{/view}', 'path' => '/manga/reading{/view}',
'action' => ['manga_list'], 'action' => ['manga_list'],
'params' => [ 'params' => [
'type' => 'Reading', 'type' => 'Reading',
@ -167,7 +174,7 @@ return [
] ]
], ],
'plan_to_read' => [ 'plan_to_read' => [
'path' => '/plan_to_read{/view}', 'path' => '/manga/plan_to_read{/view}',
'action' => ['manga_list'], 'action' => ['manga_list'],
'params' => [ 'params' => [
'type' => 'Plan to Read', 'type' => 'Plan to Read',
@ -178,7 +185,7 @@ return [
] ]
], ],
'on_hold' => [ 'on_hold' => [
'path' => '/on_hold{/view}', 'path' => '/manga/on_hold{/view}',
'action' => ['manga_list'], 'action' => ['manga_list'],
'params' => [ 'params' => [
'type' => 'On Hold', 'type' => 'On Hold',
@ -189,7 +196,7 @@ return [
] ]
], ],
'dropped' => [ 'dropped' => [
'path' => '/dropped{/view}', 'path' => '/manga/dropped{/view}',
'action' => ['manga_list'], 'action' => ['manga_list'],
'params' => [ 'params' => [
'type' => 'Dropped', 'type' => 'Dropped',
@ -200,7 +207,7 @@ return [
] ]
], ],
'completed' => [ 'completed' => [
'path' => '/completed{/view}', 'path' => '/manga/completed{/view}',
'action' => ['manga_list'], 'action' => ['manga_list'],
'params' => [ 'params' => [
'type' => 'Completed', 'type' => 'Completed',

View File

@ -1,11 +1,17 @@
{ {
"name": "timw4mail/hummingbird-anime-client",
"description": "A self-hosted anime/manga client for hummingbird.",
"license":"MIT",
"require": { "require": {
"guzzlehttp/guzzle": "5.3.*", "guzzlehttp/guzzle": "5.3.*",
"filp/whoops": "1.1.*", "filp/whoops": "dev-php7#fe32a402b086b21360e82013e8a0355575c7c6f4",
"aura/router": "2.2.*", "aura/router": "2.2.*",
"aura/web": "2.0.*", "aura/web": "2.0.*",
"aviat4ion/query": "2.0.*", "aura/html": "2.*",
"robmorgan/phinx": "*", "aura/session": "2.*",
"abeautifulsite/simpleimage": "*" "aviat4ion/query": "2.5.*",
"robmorgan/phinx": "0.4.*",
"abeautifulsite/simpleimage": "2.5.*",
"szymach/c-pchart": "1.*"
} }
} }

View File

@ -3,8 +3,6 @@
* Here begins everything! * Here begins everything!
*/ */
namespace AnimeClient;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// ! Start config // ! Start config
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -30,9 +28,42 @@ if ($timezone === '' || $timezone === FALSE)
// Define base directories // Define base directories
define('ROOT_DIR', __DIR__); define('ROOT_DIR', __DIR__);
define('APP_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'app'); 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('CONF_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'config');
define('BASE_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'base'); define('BASE_DIR', SRC_DIR . DIRECTORY_SEPARATOR . 'Base');
require BASE_DIR . 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());
}
/**
* 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
_setup_autoloaders(); _setup_autoloaders();

View File

@ -11,13 +11,7 @@
<template name="clean" /> <template name="clean" />
</transformations> </transformations>
<files> <files>
<directory>.</directory> <directory>src</directory>
<directory>app</directory> <ignore>src/views/*</ignore>
<ignore>public/*</ignore>
<ignore>app/views/*</ignore>
<ignore>app/config/*</ignore>
<ignore>migrations/*</ignore>
<ignore>tests/*</ignore>
<ignore>vendor/*</ignore>
</files> </files>
</phpdoc> </phpdoc>

View File

@ -5,15 +5,18 @@
bootstrap="tests/bootstrap.php"> bootstrap="tests/bootstrap.php">
<filter> <filter>
<whitelist> <whitelist>
<directory suffix=".php">app/base</directory> <directory suffix=".php">src/Base</directory>
<directory suffix=".php">app/controllers</directory> <directory suffix=".php">src/Controller</directory>
<directory suffix=".php">app/models</directory> <directory suffix=".php">src/Model</directory>
</whitelist> </whitelist>
</filter> </filter>
<testsuites> <testsuites>
<testsuite name="BaseTests"> <testsuite name="BaseTests">
<directory>tests/base</directory> <directory>tests</directory>
<directory>tests/Base</directory>
</testsuite> </testsuite>
<testsuite name="ModelTests"><directory>tests/Model</directory></testsuite>
<testsuite name="ControllerTests"><directory>tests/Controller</directory></testsuite>
</testsuites> </testsuites>
<php> <php>
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" /> <server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />

164
src/Base/Config.php Normal file
View File

@ -0,0 +1,164 @@
<?php
/**
* Base Configuration class
*/
namespace AnimeClient\Base;
/**
* Wrapper for configuration values
*/
class Config {
/**
* Config object
*
* @var array
*/
protected $config = [];
/**
* Constructor
*
* @param array $config_files
*/
public function __construct(Array $config_files=[])
{
// @codeCoverageIgnoreStart
if (empty($config_files))
{
require_once \_dir(CONF_DIR, 'config.php'); // $config
require_once \_dir(CONF_DIR, 'base_config.php'); // $base_config
$this->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

50
src/Base/Container.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace Animeclient\Base;
/**
* Wrapper of Aura container to be in the anime client namespace
*/
class Container {
/**
* @var array
*/
protected $container = [];
/**
* Constructor
*/
public function __construct(array $values = [])
{
$this->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

283
src/Base/Controller.php Normal file
View File

@ -0,0 +1,283 @@
<?php
/**
* Base Controller
*/
namespace AnimeClient\Base;
/**
* Base class for controllers, defines output methods
*/
class Controller {
/**
* The global configuration object
* @var object $config
*/
protected $config;
/**
* Request object
* @var object $request
*/
protected $request;
/**
* Response object
* @var object $response
*/
protected $response;
/**
* The api model for the current controller
* @var object
*/
protected $model;
/**
* Common data to be sent to views
* @var array
*/
protected $base_data = [
'url_type' => '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

112
src/Base/Model.php Normal file
View File

@ -0,0 +1,112 @@
<?php
/**
* Base for base models
*/
namespace AnimeClient\Base;
use abeautifulsite\SimpleImage;
/**
* Common base for all Models
*/
class Model {
/**
* The global configuration object
* @var Config
*/
protected $config;
/**
* The container object
* @var Container
*/
protected $container;
/**
* Constructor
*/
public function __construct(Container $container)
{
$this->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

81
src/Base/Model/API.php Normal file
View File

@ -0,0 +1,81 @@
<?php
/**
* Base API Model
*/
namespace AnimeClient\Base\Model;
use \GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar;
use \AnimeClient\Base\Container;
/**
* Base model for api interaction
*/
class API extends \AnimeClient\Base\Model {
/**
* Base url for making api requests
* @var string
*/
protected $base_url = '';
/**
* The Guzzle http client object
* @var object
*/
protected $client;
/**
* Cookie jar object for api requests
* @var object
*/
protected $cookieJar;
/**
* Constructor
*/
public function __construct(Container $container)
{
parent::__construct($container);
$this->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

34
src/Base/Model/DB.php Normal file
View File

@ -0,0 +1,34 @@
<?php
/**
* Base DB model
*/
namespace AnimeClient\Base\Model;
use AnimeClient\Base\Container;
/**
* Base model for database interaction
*/
class DB extends \AnimeClient\Base\Model {
/**
* The query builder object
* @var object $db
*/
protected $db;
/**
* The database connection information array
* @var array $db_config
*/
protected $db_config;
/**
* Constructor
*/
public function __construct(Container $container)
{
parent::__construct($container);
$this->db_config = $this->config->database;
}
}
// End of BaseDBModel.php

230
src/Base/Router.php Normal file
View File

@ -0,0 +1,230 @@
<?php
/**
* Routing logic
*/
namespace AnimeClient\Base;
use \Aura\Web\Request;
use \Aura\Web\Response;
/**
* Basic routing/ dispatch
*/
class Router {
/**
* The route-matching object
* @var object $router
*/
protected $router;
/**
* The global configuration object
* @var object $config
*/
protected $config;
/**
* Class wrapper for input superglobals
* @var object
*/
protected $request;
/**
* Array containing request and response objects
* @var array $web
*/
protected $web;
/**
* Routes added to router
* @var array $output_routes
*/
protected $output_routes;
/**
* Injection Container
* @var Container $container
*/
protected $container;
/**
* Constructor
*
* @param Config $config
* @param Router $router
* @param Request $request
* @param Response $response
*/
public function __construct(Container $container)
{
$this->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

149
src/Base/UrlGenerator.php Normal file
View File

@ -0,0 +1,149 @@
<?php
namespace AnimeClient\Base;
/**
* UrlGenerator class.
*/
class UrlGenerator {
/**
* Config Object
* @var Config
*/
protected $config;
/**
* Constructor
*
* @param Container $container
*/
public function __construct(Container $container)
{
$this->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

197
src/Controller/Anime.php Normal file
View File

@ -0,0 +1,197 @@
<?php
/**
* Anime Controller
*/
namespace AnimeClient\Controller;
use AnimeClient\Base\Container;
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 Anime extends BaseController {
/**
* The anime list model
* @var object $model
*/
protected $model;
/**
* The anime collection model
* @var object $collection_model
*/
private $collection_model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Route mapping for main navigation
* @var array $nav_routes
*/
private $nav_routes = [
'Watching' => '/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 &middot; {$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

View File

@ -0,0 +1,154 @@
<?php
/**
* Anime Collection Controller
*/
namespace AnimeClient\Controller;
use AnimeClient\Base\Container;
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 collection pages
*/
class Collection extends BaseController {
/**
* The anime collection model
* @var object $collection_model
*/
private $collection_model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Route mapping for main navigation
* @var array $nav_routes
*/
private $nav_routes = [
'Watching' => '/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 &middot; {$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

94
src/Controller/Manga.php Normal file
View File

@ -0,0 +1,94 @@
<?php
/**
* Manga Controller
*/
namespace AnimeClient\Controller;
use AnimeClient\Base\Container;
use AnimeClient\Base\Controller;
use AnimeClient\Base\Config;
use AnimeClient\Model\Manga as MangaModel;
/**
* Controller for manga list
*/
class Manga extends Controller {
/**
* The manga model
* @var object $model
*/
protected $model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Route mapping for main navigation
* @var array $nav_routes
*/
private $nav_routes = [
'Reading' => '/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

11
src/Controller/Stats.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace AnimeClient\Controller;
use AnimeClient\Base\Container;
use AnimeClient\Base\Controller;
class Stats extends Controller {
}
// End of Stats.php

239
src/Model/Anime.php Normal file
View File

@ -0,0 +1,239 @@
<?php
/**
* Anime API Model
*/
namespace AnimeClient\Model;
use AnimeClient\Base\Model\API;
/**
* Model for handling requests dealing with the anime list
*/
class Anime extends API {
/**
* The base url for api requests
* @var string $base_url
*/
protected $base_url = "https://hummingbird.me/api/v1/";
/**
* Update the selected anime
*
* @param array $data
* @return array
*/
public function update($data)
{
$data['auth_token'] = $_SESSION['hummingbird_anime_token'];
$result = $this->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

View File

@ -0,0 +1,379 @@
<?php
/**
* Anime Collection DB Model
*/
namespace AnimeClient\Model;
use AnimeClient\Base\Model\DB;
use \AnimeClient\Base\Container;
use AnimeClient\Model\Anime as AnimeModel;
/**
* Model for getting anime collection data
*/
class AnimeCollection extends DB {
/**
* Anime API Model
* @var object $anime_model
*/
private $anime_model;
/**
* Whether the database is valid for querying
* @var bool
*/
private $valid_database = FALSE;
/**
* Constructor
*/
public function __construct(Container $container)
{
parent::__construct($container);
$this->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

192
src/Model/Manga.php Normal file
View File

@ -0,0 +1,192 @@
<?php
/**
* Manga API Model
*/
namespace AnimeClient\Model;
use AnimeClient\Base\Model\API;
/**
* Model for handling requests dealing with the manga list
*/
class Manga extends API {
/**
* The base url for api requests
* @var string
*/
protected $base_url = "https://hummingbird.me/";
/**
* Update the selected manga
*
* @param array $data
* @return array
*/
public function update($data)
{
$id = $data['id'];
unset($data['id']);
$result = $this->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

27
src/Model/Stats.php Normal file
View File

@ -0,0 +1,27 @@
<?php
/**
* Anime API Model
*/
namespace AnimeClient\Model;
use AnimeClient\Base\Model\DB;
use AnimeClient\Base\Container;
use StatsChartsTrait;
class Stats extends DB {
/**
* Constructor
*
* @param Config $config
*/
public function __construct(Container $container)
{
parent::__construct($container);
$this->chartSetup();
}
}
// End of Stats.php

View File

@ -0,0 +1,29 @@
<?php
namespace AnimeClient\Model;
use CpChart\Services\pChartFactory;
/**
* Trait for chart generation
*/
trait StatsChartsTrait {
/**
* @var pChartFactory
*/
protected $pchart;
/**
* Initial setup method
*
* @return void
*/
protected function chartSetup()
{
$this->pchart = new pChartFactory();
}
}
// End of StatsChartsTrait.php

68
src/functions.php Normal file
View File

@ -0,0 +1,68 @@
<?php
/**
* Global functions
*/
/**
* Check if the user is currently logged in
*
* @return bool
*/
function is_logged_in()
{
return array_key_exists('hummingbird_anime_token', $_SESSION);
}
/**
* HTML selection helper function
*
* @param string $a - First item to compare
* @param string $b - Second item to compare
* @return string
*/
function is_selected($a, $b)
{
return ($a === $b) ? 'selected' : '';
}
/**
* Inverse of selected helper function
*
* @param string $a - First item to compare
* @param string $b - Second item to compare
* @return string
*/
function is_not_selected($a, $b)
{
return ($a !== $b) ? 'selected' : '';
}
/**
* Get the last segment of the current url
*
* @return string
*/
function last_segment()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', $path);
return end($segments);
}
/**
* Determine whether to show the sub-menu
*
* @return bool
*/
function is_view_page()
{
$blacklist = ['edit', 'add', 'update', 'login', 'logout'];
$page_segments = explode("/", $_SERVER['REQUEST_URI']);
$intersect = array_intersect($page_segments, $blacklist);
return empty($intersect);
}
// End of functions.php

View File

@ -12,9 +12,9 @@
<body class="<?= $url_type ?> list"> <body class="<?= $url_type ?> list">
<h1 class="flex flex-align-end flex-wrap"> <h1 class="flex flex-align-end flex-wrap">
<span class="flex-no-wrap grow-1"> <span class="flex-no-wrap grow-1">
<a href="<?= $config->full_url("", $url_type) ?>"> <a href="<?= $config->default_url($url_type) ?>">
<?= WHOSE ?> <?= ucfirst($url_type) ?> <?= (strpos($route_path, 'collection') !== FALSE) ? 'Collection' : 'List' ?> <?= WHOSE ?> <?= ucfirst($url_type) ?> <?= (strpos($route_path, 'collection') !== FALSE) ? 'Collection' : 'List' ?>
</a> [<a href="<?= $config->full_url("", $other_type) ?>"><?= ucfirst($other_type) ?> List</a>] </a> [<a href="<?= $config->default_url($other_type) ?>"><?= ucfirst($other_type) ?> List</a>]
</span> </span>
<span class="flex-no-wrap small-font"> <span class="flex-no-wrap small-font">
<?php if (is_logged_in()): ?> <?php if (is_logged_in()): ?>
@ -28,14 +28,14 @@
<nav> <nav>
<ul> <ul>
<?php foreach($nav_routes as $title => $nav_path): ?> <?php foreach($nav_routes as $title => $nav_path): ?>
<li class="<?= is_selected($nav_path, $route_path) ?>"><a href="<?= $config->full_url($nav_path, $url_type) ?>"><?= $title ?></a></li> <li class="<?= is_selected($nav_path, $route_path) ?>"><a href="<?= $config->url($nav_path) ?>"><?= $title ?></a></li>
<?php endforeach ?> <?php endforeach ?>
</ul> </ul>
<?php if (is_view_page()): ?> <?php if (is_view_page()): ?>
<br /> <br />
<ul> <ul>
<li class="<?= is_not_selected('list', last_segment()) ?>"><a href="<?= $config->full_url($route_path, $url_type) ?>">Cover View</a></li> <li class="<?= is_not_selected('list', last_segment()) ?>"><a href="<?= $config->url($route_path) ?>">Cover View</a></li>
<li class="<?= is_selected('list', last_segment()) ?>"><a href="<?= $config->full_url("{$route_path}/list", $url_type) ?>">List View</a></li> <li class="<?= is_selected('list', last_segment()) ?>"><a href="<?= $config->url("{$route_path}/list") ?>">List View</a></li>
</ul> </ul>
<?php endif ?> <?php endif ?>
</nav> </nav>

View File

@ -0,0 +1,61 @@
<?php
use \AnimeClient\Base\Controller;
use \Aura\Web\WebFactory;
use \Aura\Router\RouterFactory;
class BaseControllerTest extends AnimeClient_TestCase {
public function setUp()
{
parent::setUp();
// Create Request/Response Objects
$web_factory = new WebFactory([
'_GET' => [],
'_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);
}
}

View File

@ -1,12 +1,13 @@
<?php <?php
use \AnimeClient\BaseModel; use AnimeClient\Base\Model as BaseModel;
use AnimeClient\Base\Container;
class BaseModelTest extends AnimeClient_TestCase { class BaseModelTest extends AnimeClient_TestCase {
public function testBaseModelSanity() public function testBaseModelSanity()
{ {
$baseModel = new BaseModel($this->config); $baseModel = new BaseModel($this->container);
$this->assertTrue(is_object($baseModel)); $this->assertTrue(is_object($baseModel));
} }
} }

View File

@ -1,19 +1,15 @@
<?php <?php
use \AnimeClient\Config; use \AnimeClient\Base\Config;
class ConfigTest extends AnimeClient_TestCase { class ConfigTest extends AnimeClient_TestCase {
public function setUp() public function setUp()
{ {
$this->config = new Config([ $this->config = new Config([
'config' => [
'foo' => 'bar', 'foo' => 'bar',
'asset_path' => '//localhost/assets/' 'asset_path' => '//localhost/assets/',
],
'base_config' => [
'bar' => 'baz' 'bar' => 'baz'
]
]); ]);
} }
@ -57,37 +53,35 @@ class ConfigTest extends AnimeClient_TestCase {
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result);
} }
public function fullUrlProvider() public function dataFullUrl()
{ {
return [ return [
'default_view' => [ 'default_view' => [
'config' => [ 'config' => [
'anime_host' => '', 'routing' => [
'manga_host' => '',
'anime_path' => 'anime', 'anime_path' => 'anime',
'manga_path' => 'manga', 'manga_path' => 'manga',
'route_by' => 'host',
'default_list' => 'manga', 'default_list' => 'manga',
'default_anime_path' => '/watching', 'default_anime_path' => '/anime/watching',
'default_manga_path' => '/all', 'default_manga_path' => '/manga/all',
'default_to_list_view' => FALSE, 'default_to_list_view' => FALSE,
], ],
],
'path' => '', 'path' => '',
'type' => 'manga', 'type' => 'manga',
'expected' => '//localhost/manga/all', 'expected' => '//localhost/manga/all',
], ],
'default_view_list' => [ 'default_view_list' => [
'config' => [ 'config' => [
'anime_host' => '', 'routing' => [
'manga_host' => '',
'anime_path' => 'anime', 'anime_path' => 'anime',
'manga_path' => 'manga', 'manga_path' => 'manga',
'route_by' => 'host',
'default_list' => 'manga', 'default_list' => 'manga',
'default_anime_path' => '/watching', 'default_anime_path' => '/anime/watching',
'default_manga_path' => '/all', 'default_manga_path' => '/manga/all',
'default_to_list_view' => TRUE, 'default_to_list_view' => TRUE,
], ],
],
'path' => '', 'path' => '',
'type' => 'manga', 'type' => 'manga',
'expected' => '//localhost/manga/all/list', 'expected' => '//localhost/manga/all/list',
@ -96,14 +90,52 @@ class ConfigTest extends AnimeClient_TestCase {
} }
/** /**
* @dataProvider fullUrlProvider * @dataProvider dataFullUrl
*/ */
public function testFullUrl($config, $path, $type, $expected) 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); $result = $this->config->full_url($path, $type);
$this->assertEquals($expected, $result); $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);
}
} }

View File

@ -1,12 +1,13 @@
<?php <?php
use \AnimeClient\BaseApiModel; use AnimeClient\Base\Container;
use AnimeClient\Base\Model\API as BaseApiModel;
class MockBaseApiModel extends BaseApiModel { class MockBaseApiModel extends BaseApiModel {
public function __construct(\AnimeClient\Config $config) public function __construct(Container $container)
{ {
parent::__construct($config); parent::__construct($container);
} }
public function __get($key) public function __get($key)
@ -19,11 +20,11 @@ class BaseApiModelTest extends AnimeClient_TestCase {
public function testBaseApiModelSanity() public function testBaseApiModelSanity()
{ {
$baseApiModel = new MockBaseApiModel($this->config); $baseApiModel = new MockBaseApiModel($this->container);
// Some basic type checks for class memebers // Some basic type checks for class memebers
$this->assertInstanceOf('\AnimeClient\BaseModel', $baseApiModel); $this->assertInstanceOf('\AnimeClient\Base\Model', $baseApiModel);
$this->assertInstanceOf('\AnimeClient\BaseApiModel', $baseApiModel); $this->assertInstanceOf('\AnimeClient\Base\Model\API', $baseApiModel);
$this->assertInstanceOf('\GuzzleHttp\Client', $baseApiModel->client); $this->assertInstanceOf('\GuzzleHttp\Client', $baseApiModel->client);
$this->assertInstanceOf('\GuzzleHttp\Cookie\CookieJar', $baseApiModel->cookieJar); $this->assertInstanceOf('\GuzzleHttp\Cookie\CookieJar', $baseApiModel->cookieJar);

View File

@ -1,12 +1,12 @@
<?php <?php
use \AnimeClient\BaseDBModel; use AnimeClient\Base\Model\DB as BaseDBModel;
class BaseDBModelTest extends AnimeClient_TestCase { class BaseDBModelTest extends AnimeClient_TestCase {
public function testBaseDBModelSanity() public function testBaseDBModelSanity()
{ {
$baseDBModel = new BaseDBModel($this->config); $baseDBModel = new BaseDBModel($this->container);
$this->assertTrue(is_object($baseDBModel)); $this->assertTrue(is_object($baseDBModel));
} }
} }

193
tests/Base/RouterTest.php Normal file
View File

@ -0,0 +1,193 @@
<?php
use AnimeClient\Base\Router;
use AnimeClient\Base\Config;
use AnimeClient\Base\Container;
use Aura\Web\WebFactory;
use Aura\Router\RouterFactory;
class RouterTest extends AnimeClient_TestCase {
protected $container;
protected $router;
protected $config;
protected function _set_up($config, $uri, $host)
{
// Set up the environment
$_SERVER = array_merge($_SERVER, [
'REQUEST_METHOD' => '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 &middot; 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 &middot; 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");
}
}

View File

@ -0,0 +1,7 @@
<?php
class AnimeCollectionModelTest extends AnimeClient_TestCase {
}

View File

@ -1,31 +0,0 @@
<?php
use \AnimeClient\BaseController;
use \Aura\Web\WebFactory;
use \Aura\Router\RouterFactory;
class BaseControllerTest extends AnimeClient_TestCase {
public function setUp()
{
parent::setUp();
// Create Request/Response Objects
$web_factory = new WebFactory([
'_GET' => [],
'_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));
}
}

View File

@ -1,250 +0,0 @@
<?php
use AnimeClient\Router;
use AnimeClient\Config;
use Aura\Web\WebFactory;
use Aura\Router\RouterFactory;
class RouterTest extends AnimeClient_TestCase {
protected $aura_router;
protected $request;
protected $response;
protected $router;
public function testRouterSanity()
{
parent::setUp();
$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);
$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 &middot; 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 &middot; 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'));
}*/
}

View File

@ -3,7 +3,8 @@
* Global setup for unit tests * Global setup for unit tests
*/ */
use \AnimeClient\Config; use AnimeClient\Base\Config;
use AnimeClient\Base\Container;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Mock the default error handler // Mock the default error handler
@ -23,28 +24,27 @@ $defaultHandler = new MockErrorHandler();
* Base class for TestCases * Base class for TestCases
*/ */
class AnimeClient_TestCase extends PHPUnit_Framework_TestCase { class AnimeClient_TestCase extends PHPUnit_Framework_TestCase {
protected $container;
protected $config;
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
global $config; $config = new Config([
$this->config = new Config([ 'asset_path' => '//localhost/assets/',
'config' => [
'asset_path' => '//localhost/assets/'
],
'base_config' => [
'databaase' => [], 'databaase' => [],
'routes' => [ 'routes' => [
'common' => [], 'common' => [],
'anime' => [], 'anime' => [],
'manga' => [] '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 base path constants
define('ROOT_DIR', realpath(__DIR__ . DIRECTORY_SEPARATOR . "/../")); 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('APP_DIR', _dir(ROOT_DIR, 'app'));
define('CONF_DIR', _dir(APP_DIR, 'config')); 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
_setup_autoloaders(); _setup_autoloaders();
require(_dir(BASE_DIR, 'functions.php')); require(_dir(SRC_DIR, 'functions.php'));
// Pre-define some superglobals // Pre-define some superglobals
$_SESSION = []; $_SESSION = [];