Browse Source

Some progress toward better structure through refactoring

tags/v2
Timothy Warren 4 years ago
parent
commit
cee211621c
65 changed files with 3114 additions and 471 deletions
  1. +20
    -0
      .editorconfig
  2. +2
    -2
      app/Base/ApiModel.php
  3. +7
    -4
      app/Base/Config.php
  4. +2
    -2
      app/Base/Controller.php
  5. +2
    -2
      app/Base/DBModel.php
  6. +2
    -2
      app/Base/Model.php
  7. +5
    -14
      app/Base/Router.php
  8. +0
    -0
      app/Base/functions.php
  9. +5
    -10
      app/Base/pre_conf_functions.php
  10. +7
    -2
      app/Controller/Anime.php
  11. +6
    -2
      app/Controller/Manga.php
  12. +5
    -2
      app/Model/Anime.php
  13. +6
    -2
      app/Model/AnimeCollection.php
  14. +5
    -2
      app/Model/Manga.php
  15. +20
    -5
      app/bootstrap.php
  16. +31
    -13
      app/config/config.php
  17. +50
    -43
      app/config/routes.php
  18. +10
    -4
      composer.json
  19. +35
    -4
      index.php
  20. +2
    -8
      phpdoc.dist.xml
  21. +7
    -4
      phpunit.xml
  22. +164
    -0
      src/Base/Config.php
  23. +50
    -0
      src/Base/Container.php
  24. +283
    -0
      src/Base/Controller.php
  25. +112
    -0
      src/Base/Model.php
  26. +81
    -0
      src/Base/Model/API.php
  27. +34
    -0
      src/Base/Model/DB.php
  28. +230
    -0
      src/Base/Router.php
  29. +149
    -0
      src/Base/UrlGenerator.php
  30. +197
    -0
      src/Controller/Anime.php
  31. +154
    -0
      src/Controller/Collection.php
  32. +94
    -0
      src/Controller/Manga.php
  33. +11
    -0
      src/Controller/Stats.php
  34. +239
    -0
      src/Model/Anime.php
  35. +379
    -0
      src/Model/AnimeCollection.php
  36. +192
    -0
      src/Model/Manga.php
  37. +27
    -0
      src/Model/Stats.php
  38. +29
    -0
      src/Model/StatsChartsTrait.php
  39. +68
    -0
      src/functions.php
  40. +0
    -0
      src/views/404.php
  41. +0
    -0
      src/views/anime/cover.php
  42. +0
    -0
      src/views/anime/edit.php
  43. +0
    -0
      src/views/anime/list.php
  44. +0
    -0
      src/views/collection/add.php
  45. +0
    -0
      src/views/collection/cover.php
  46. +0
    -0
      src/views/collection/edit.php
  47. +0
    -0
      src/views/collection/list.php
  48. +0
    -0
      src/views/footer.php
  49. +5
    -5
      src/views/header.php
  50. +0
    -0
      src/views/login.php
  51. +0
    -0
      src/views/manga/cover.php
  52. +0
    -0
      src/views/manga/list.php
  53. +0
    -0
      src/views/message.php
  54. +61
    -0
      tests/Base/BaseControllerTest.php
  55. +3
    -2
      tests/Base/BaseModelTest.php
  56. +61
    -29
      tests/Base/ConfigTest.php
  57. +0
    -0
      tests/Base/CoreTest.php
  58. +7
    -6
      tests/Base/Model/BaseApiModelTest.php
  59. +2
    -2
      tests/Base/Model/BaseDBModelTest.php
  60. +193
    -0
      tests/Base/RouterTest.php
  61. +0
    -0
      tests/FunctionsTest.php
  62. +7
    -0
      tests/Model/AnimeCollectionModelest.php
  63. +0
    -31
      tests/base/BaseControllerTest.php
  64. +0
    -250
      tests/base/RouterTest.php
  65. +53
    -19
      tests/bootstrap.php

+ 20
- 0
.editorconfig 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

app/base/BaseApiModel.php → app/Base/ApiModel.php View File

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

app/base/Config.php → app/Base/Config.php View File

@@ -1,6 +1,9 @@
<?php
/**
* Base Configuration class
*/

namespace AnimeClient;
namespace AnimeClient\Base;

/**
* Wrapper for configuration values
@@ -58,7 +61,7 @@ class Config {
*
* @return string
*/
function asset_url(/*...*/)
public function asset_url(/*...*/)
{
$args = func_get_args();
$base_url = rtrim($this->__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");

app/base/BaseController.php → app/Base/Controller.php View File

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

app/base/BaseDBModel.php → app/Base/DBModel.php View File

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

app/base/BaseModel.php → app/Base/Model.php View File

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

app/base/Router.php → app/Base/Router.php View File

@@ -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 = [];

app/base/functions.php → app/Base/functions.php View File


app/base/pre_conf_functions.php → app/Base/pre_conf_functions.php View File

@@ -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;
}
});
}

app/controllers/AnimeController.php → app/Controller/Anime.php View File

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

app/controllers/MangaController.php → app/Controller/Manga.php View File

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

app/models/AnimeModel.php → app/Model/Anime.php View File

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

app/models/AnimeCollectionModel.php → app/Model/AnimeCollection.php View File

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

app/models/MangaModel.php → app/Model/Manga.php View File

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

+ 20
- 5
app/bootstrap.php View File

@@ -1,4 +1,7 @@
<?php
/**
* Bootstrap / Dependency Injection
*/

namespace AnimeClient;

@@ -6,6 +9,15 @@ use \Whoops\Handler\PrettyPageHandler;
use \Whoops\Handler\JsonResponseHandler;
use \Aura\Web\WebFactory;
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
@@ -23,17 +35,20 @@ $whoops->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

+ 31
- 13
app/config/config.php View File

@@ -1,4 +1,4 @@
<?php
<?php
$config = [
// ----------------------------------------------------------------------------
// Username for anime and manga lists
@@ -12,32 +12,50 @@ $config = [
// do you wish to show the anime collection tab?
'show_anime_collection' => 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,

+ 50
- 43
app/config/routes.php View File

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

+ 10
- 4
composer.json View File

@@ -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.*"
}
}

+ 35
- 4
index.php View File

@@ -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();

+ 2
- 8
phpdoc.dist.xml View File

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

+ 7
- 4
phpunit.xml View File

@@ -5,15 +5,18 @@
bootstrap="tests/bootstrap.php">
<filter>
<whitelist>
<directory suffix=".php">app/base</directory>
<directory suffix=".php">app/controllers</directory>
<directory suffix=".php">app/models</directory>
<directory suffix=".php">src/Base</directory>
<directory suffix=".php">src/Controller</directory>
<directory suffix=".php">src/Model</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="BaseTests">
<directory>tests/base</directory>
<directory>tests</directory>
<directory>tests/Base</directory>
</testsuite>
<testsuite name="ModelTests"><directory>tests/Model</directory></testsuite>
<testsuite name="ControllerTests"><directory>tests/Controller</directory></testsuite>
</testsuites>
<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" />

+ 164
- 0
src/Base/Config.php 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
- 0
src/Base/Container.php 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
- 0
src/Base/Controller.php 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
- 0
src/Base/Model.php 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
- 0
src/Base/Model/API.php 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
- 0
src/Base/Model/DB.php 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
- 0
src/Base/Router.php 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
- 0
src/Base/UrlGenerator.php 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
- 0
src/Controller/Anime.php 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

+ 154
- 0
src/Controller/Collection.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'