Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
24 changed files with 631 additions and 220 deletions
Showing only changes of commit 5e5434d057 - Show all commits

View File

@ -31,14 +31,14 @@ A self-hosted client that allows custom formatting of data from the hummingbird
### Requirements
* PHP 5.4+
* PHP 5.5+
* PDO SQLite (For collection tab)
* GD
### Installation
1. Install dependencies via composer: `composer install`
2. Configure settings in `app/config/config.php` and `app/config/routing.php` to your liking
2. Configure settings in `app/config/config.php` to your liking
3. Create the following directories if they don't exist, and make sure they are world writable
* app/cache
* public/images/manga

View File

@ -7,6 +7,7 @@ namespace Aviat\AnimeClient;
use \Whoops\Handler\PrettyPageHandler;
use \Whoops\Handler\JsonResponseHandler;
use Aura\Html\HelperLocatorFactory;
use \Aura\Web\WebFactory;
use \Aura\Router\RouterFactory;
use \Aura\Session\SessionFactory;
@ -51,6 +52,15 @@ $di = function() {
$aura_router = (new RouterFactory())->newInstance();
$container->set('aura-router', $aura_router);
// Create Html helper Object
$html_helper = (new HelperLocatorFactory)->newInstance();
$html_helper->set('menu', function() use ($container) {
$menu_helper = new Helper\Menu();
$menu_helper->setContainer($container);
return $menu_helper;
});
$container->set('html-helper', $html_helper);
// Create Request/Response Objects
$web_factory = new WebFactory([
'_GET' => $_GET,
@ -79,4 +89,4 @@ $di = function() {
$di()->get('router')->dispatch();
// End of bootstrap.php
// End of bootstrap.php

View File

@ -13,6 +13,7 @@ $base_config = [
'img_cache_path' => _dir(ROOT_DIR, 'public/images'),
// Included config files
'routes' => require __DIR__ . '/routes.php',
'database' => require __DIR__ . '/database.php',
'menus' => require __DIR__ . '/menus.php',
'routes' => require __DIR__ . '/routes.php',
];

View File

@ -14,9 +14,12 @@ $config = [
// General config
// ----------------------------------------------------------------------------
// do you wish to show the anime collection tab?
// do you wish to show the anime collection?
'show_anime_collection' => TRUE,
// do you wish to show the manga collection?
'show_manga_collection' => TRUE,
// path to public directory on the server
'asset_dir' => realpath(__DIR__ . '/../../public'),

61
app/config/menus.php Normal file
View File

@ -0,0 +1,61 @@
<?php
return [
'top' => [
'default' => '',
'items' => [
'anime_list' => '{anime_list}',
'manga_list' => '{manga_list}',
'collection' => '{collection}'
]
],
'view_type' => [
'is_parent' => FALSE,
'default' => 'cover_view',
'items' => [
'cover_view' => '{parent}',
'list_view' => '{parent}/list'
]
],
'anime_list' => [
'default' => '',
'route_prefix' => '/anime',
'items' => [
'watching' => '/watching',
'plan_to_watch' => '/plan_to_watch',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
],
'children' => [
'view_type'
]
],
'manga_list' => [
'default' => '',
'route_prefix' => '/manga',
'items' => [
'reading' => '/reading',
'plan_to_read' => '/plan_to_read',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
],
'children' => [
'view_type'
]
],
'collection' => [
'default' => '',
'route_prefix' => '/collection',
'items' => [
'anime' => '/anime',
'manga' => '/manga',
],
'children' => [
'view_type'
]
]
];

View File

@ -9,99 +9,56 @@ return [
// Routes on all controllers
'common' => [
'update' => [
'path' => '/update',
'action' => ['update'],
'path' => '/{controller}/update',
'action' => 'update',
'verb' => 'post'
],
'login_form' => [
'path' => '/login',
'action' => ['login'],
'path' => '/{controller}/login',
'action' => 'login',
'verb' => 'get'
],
'login_action' => [
'path' => '/login',
'action' => ['login_action'],
'path' => '/{controller}/login',
'action' => 'login_action',
'verb' => 'post'
],
'logout' => [
'path' => '/logout',
'action' => ['logout']
'path' => '/{controller}/logout',
'action' => 'logout'
],
],
// Routes on collection controller
'collection' => [
'collection_add_form' => [
'path' => '/collection/add',
'action' => ['form'],
'action' => 'form',
'params' => [],
],
'collection_edit_form' => [
'path' => '/collection/edit/{id}',
'action' => ['form'],
'action' => 'form',
'tokens' => [
'id' => '[0-9]+'
]
],
'collection_add' => [
'path' => '/collection/add',
'action' => ['add'],
'action' => 'add',
'verb' => 'post'
],
'collection_edit' => [
'path' => '/collection/edit',
'action' => ['edit'],
'action' => 'edit',
'verb' => 'post'
],
'collection' => [
'path' => '/collection/view{/view}',
'action' => ['index'],
'action' => 'index',
'params' => [],
'tokens' => [
'view' => '[a-z_]+'
]
],
],
// Routes on anime controller
'anime' => [
'index' => [
'path' => '/',
'action' => ['redirect'],
'params' => [
'url' => '', // Determined by config
'code' => '301',
'type' => 'anime'
]
],
'search' => [
'path' => '/anime/search',
'action' => ['search'],
],
'anime_list' => [
'path' => '/anime/{type}{/view}',
'action' => ['anime_list'],
'tokens' => [
'type' => '[a-z_]+',
'view' => '[a-z_]+'
]
],
],
'manga' => [
'index' => [
'path' => '/',
'action' => ['redirect'],
'params' => [
'url' => '', // Determined by config
'code' => '301',
'type' => 'manga'
]
],
'manga_list' => [
'path' => '/manga/{type}{/view}',
'action' => ['manga_list'],
'tokens' => [
'type' => '[a-z_]+',
'view' => '[a-z_]+'
]
]
]
];

View File

@ -5,7 +5,7 @@
// ----------------------------------------------------------------------------
return [
// Subfolder prefix for url
// Subfolder prefix for url, if in a subdirectory of the web root
'subfolder_prefix' => '',
// Path to public directory, where images/css/javascript are located,
@ -16,9 +16,9 @@ return [
'default_list' => 'anime', // anime or manga
// Default pages for anime/manga
'default_anime_path' => "/anime/watching",
'default_manga_path' => '/manga/all',
'default_anime_list_path' => "watching", // watching|plan_to_watch|on_hold|dropped|completed|all
'default_manga_list_path' => "all", // reading|plan_to_read|on_hold|dropped|completed|all
// Default to list view?
'default_to_list_view' => FALSE,
// Default view type (cover_view/list_view)
'default_view_type' => 'cover_view',
];

View File

@ -11,6 +11,6 @@
<template name="clean" />
</transformations>
<files>
<directory>src/Aviat/AnimeClient</directory>
<directory>src/Aviat</directory>
</files>
</phpdoc>

View File

@ -4,8 +4,6 @@
*/
namespace Aviat\AnimeClient;
use Aura\Web\ResponseSender;
use \Aviat\Ion\Di\ContainerInterface;
use \Aviat\Ion\View\HttpView;
use \Aviat\Ion\View\HtmlView;
@ -97,6 +95,7 @@ class Controller {
public function load_partial($view, $template, $data=[])
{
$errorHandler = $this->container->get('error-handler');
$errorHandler->addDataTable('Template Data', $data);
$router = $this->container->get('router');
if (isset($this->base_data))
@ -107,7 +106,7 @@ class Controller {
$route = $router->get_route();
$data['route_path'] = ($route) ? $router->get_route()->path : "";
$errorHandler->addDataTable('Template Data', $data);
$template_path = _dir($this->config->__get('view_path'), "{$template}.php");
if ( ! is_file($template_path))
@ -170,7 +169,7 @@ class Controller {
{
$url = $this->urlGenerator->full_url($path, $type);
$http = new HttpView($this->container);
$http->redirect($url, $code);
}

View File

@ -7,6 +7,7 @@ namespace Aviat\AnimeClient\Controller;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Enum\Hummingbird\AnimeWatchingStatus;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Model\AnimeCollection as AnimeCollectionModel;
@ -72,6 +73,11 @@ class Anime extends BaseController {
]);
}
public function index($type="watching", $view='')
{
return $this->anime_list($type, $view);
}
/**
* Search for anime
*
@ -87,11 +93,10 @@ class Anime extends BaseController {
* 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
* @param string $view - List or cover view
* @return void
*/
public function anime_list($type, $view)
protected function anime_list($type, $view)
{
$type_title_map = [
'all' => 'All',
@ -103,12 +108,12 @@ class Anime extends BaseController {
];
$model_map = [
'watching' => 'currently-watching',
'plan_to_watch' => 'plan-to-watch',
'on_hold' => 'on-hold',
'watching' => AnimeWatchingStatus::WATCHING,
'plan_to_watch' => AnimeWatchingStatus::PLAN_TO_WATCH,
'on_hold' => AnimeWatchingStatus::ON_HOLD,
'all' => 'all',
'dropped' => 'dropped',
'completed' => 'completed'
'dropped' => AnimeWatchingStatus::DROPPED,
'completed' => AnimeWatchingStatus::COMPLETED
];
$title = $this->config->whose_list . "'s Anime List &middot; {$type_title_map[$type]}";

View File

@ -58,6 +58,11 @@ class Manga extends Controller {
]);
}
public function index($status="all", $view="")
{
return $this->manga_list($status, $view);
}
/**
* Update an anime item
*
@ -75,15 +80,15 @@ class Manga extends Controller {
* @param string $view
* @return void
*/
public function manga_list($status, $view)
protected function manga_list($status, $view)
{
$map = [
'all' => 'All',
'plan_to_read' => 'Plan to Read',
'reading' => 'Reading',
'completed' => 'Completed',
'dropped' => 'Dropped',
'on_hold' => 'On Hold'
'plan_to_read' => MangaModel::PLAN_TO_READ,
'reading' => MangaModel::READING,
'completed' => MangaModel::COMPLETED,
'dropped' => MangaModel::DROPPED,
'on_hold' => MangaModel::ON_HOLD
];
$title = $this->config->whose_list . "'s Manga List &middot; {$map[$status]}";
@ -97,10 +102,12 @@ class Manga extends Controller {
? [$map[$status] => $this->model->get_list($map[$status])]
: $this->model->get_all_lists();
//throw new \ErrorException("Data :" . print_r($data, TRUE));
$this->outputHTML('manga/' . $view_map[$view], [
'title' => $title,
'sections' => $data,
]);
}
}
// End of MangaController.php
// End of MangaController.php

View File

@ -0,0 +1,20 @@
<?php
namespace Aviat\AnimeClient\Helper;
use Aura\Html\Helper\AbstractHelper;
use Aviat\AnimeClient\MenuGenerator;
class Menu extends AbstractHelper {
use \Aviat\Ion\Di\ContainerAware;
public function __invoke($menu_name)
{
$generator = new MenuGenerator($this->container);
return $generator->generate($menu_name);
}
}
// End of Menu.php

View File

@ -0,0 +1,19 @@
<?php
namespace Aviat\AnimeClient\Helper;
use Aura\Html\Helper\AbstractHelper;
use Aviat\AnimeClient\UrlGenerator;
class UrlHelper extends AbstractHelper {
/**
* Helper entry point
*
* @return UrlHelper
*/
public function __invoke()
{
return $this;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
/**
* Helper object to manage menu creation and selection
*/
class MenuGenerator extends RoutingBase {
use \Aviat\Ion\Di\ContainerAware;
use \Aviat\Ion\StringWrapper;
use \Aviat\Ion\ArrayWrapper;
/**
* Html generation helper
*
* @var Aura\Html\HelperLocator
*/
protected $helper;
/**
* Menu config array
*
* @var array
*/
protected $menus;
/**
* Create menu generator
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->menus = $this->config->menus;
$this->helper = $container->get('html-helper');
}
/**
* Generate the full menu structure from the config files
*
* @return array
*/
protected function parse_config()
{
// Note: Children menus have urls based on the
// current url path
/*
$parsed = [
'menu_name' => [
'items' => [
'title' => 'full_url_path',
],
'children' => [
'title' => 'full_url_path'
]
]
]
*/
$parsed = [];
foreach($this->menus as $name => $menu)
{
$parsed[$name] = [];
foreach($menu['items'] as $path_name => $partial_path)
{
$title = $this->string($path_name)->humanize()->titleize();
$parsed[$name]['items'][$title] = $this->string($menu['route_prefix'])->append($partial_path);
}
// @TODO: Handle child menu(s)
if (count($menu['children']) > 0)
{
}
}
return $parsed;
}
/**
* Generate the html structure of the menu selected
*
* @param string $menu
* @return string
*/
public function generate($menu)
{
$parsed_config = $this->parse_config();
$menu_config = $parsed_config[$menu];
// Array of list items to add to the main menu
$main_menu = [];
// Start the menu list
$helper->ul();
}
}
// End of MenuGenerator.php

View File

@ -4,14 +4,16 @@
*/
namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
use abeautifulsite\SimpleImage;
use Aviat\Ion\Di\ContainerInterface;
/**
* Common base for all Models
*/
class Model {
use \Aviat\Ion\StringWrapper;
/**
* The global configuration object
* @var Config
@ -65,11 +67,11 @@ class Model {
// Cache the file if it doesn't already exist
if ( ! file_exists($cached_path))
{
if (ini_get('allow_url_fopen'))
/*if (ini_get('allow_url_fopen'))
{
copy($api_path, $cached_path);
}
elseif (function_exists('curl_init'))
else*/if (function_exists('curl_init'))
{
$ch = curl_init($api_path);
$fp = fopen($cached_path, 'wb');
@ -79,7 +81,7 @@ class Model {
]);
curl_exec($ch);
curl_close($ch);
fclose($ch);
fclose($fp);
}
else
{

View File

@ -4,8 +4,10 @@
*/
namespace Aviat\AnimeClient\Model;
use \GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Request;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Model as BaseModel;
@ -42,7 +44,8 @@ class API extends BaseModel {
parent::__construct($container);
$this->cookieJar = new CookieJar();
$this->client = new Client([
'base_url' => $this->base_url,
'base_uri' => $this->base_url,
'cookies' => TRUE,
'defaults' => [
'cookies' => $this->cookieJar,
'headers' => [

View File

@ -27,6 +27,18 @@ class Anime extends API {
*/
protected $base_url = "https://hummingbird.me/api/v1/";
/**
* Map of API status constants to display constants
* @var array
*/
protected $const_map = [
AnimeWatchingStatus::WATCHING => self::WATCHING,
AnimeWatchingStatus::PLAN_TO_WATCH => self::PLAN_TO_WATCH,
AnimeWatchingStatus::ON_HOLD => self::ON_HOLD,
AnimeWatchingStatus::DROPPED => self::DROPPED,
AnimeWatchingStatus::COMPLETED => self::COMPLETED,
];
/**
* Update the selected anime
*
@ -59,32 +71,11 @@ class Anime extends API {
self::COMPLETED => [],
];
$data = $this->_get_list();
$data = $this->_get_list_from_api();
foreach($data as $datum)
{
switch($datum['status'])
{
case AnimeWatchingStatus::COMPLETED:
$output[self::COMPLETED][] = $datum;
break;
case AnimeWatchingStatus::PLAN_TO_WATCH:
$output[self::PLAN_TO_WATCH][] = $datum;
break;
case AnimeWatchingStatus::DROPPED:
$output[self::DROPPED][] = $datum;
break;
case AnimeWatchingStatus::ON_HOLD:
$output[self::ON_HOLD][] = $datum;
break;
case AnimeWatchingStatus::WATCHING:
$output[self::WATCHING][] = $datum;
break;
}
$output[$this->const_map[$datum['watching_status']]][] = $datum;
}
// Sort anime by name
@ -104,19 +95,11 @@ class Anime extends API {
*/
public function get_list($status)
{
$map = [
AnimeWatchingStatus::WATCHING => self::WATCHING,
AnimeWatchingStatus::PLAN_TO_WATCH => self::PLAN_TO_WATCH,
AnimeWatchingStatus::ON_HOLD => self::ON_HOLD,
AnimeWatchingStatus::DROPPED => self::DROPPED,
AnimeWatchingStatus::COMPLETED => self::COMPLETED,
];
$data = $this->_get_list_From_api($status);
$this->sort_by_name($data);
$output = [];
$output[$map[$status]] = $data;
$output[$this->const_map[$status]] = $data;
return $output;
}
@ -170,10 +153,11 @@ class Anime extends API {
/**
* Retrieve data from the api
*
* @codeCoverageIgnore
* @param string $status
* @return array
*/
private function _get_list_from_api($status="all")
protected function _get_list_from_api($status="all")
{
$config = [
'allow_redirects' => FALSE
@ -198,29 +182,30 @@ class Anime extends API {
/**
* Handle caching of transformed api data
*
* @codeCoverageIgnore
* @param string $status
* @param \GuzzleHttp\Message\Response
* @return array
*/
private function _check_cache($status, $response)
protected function _check_cache($status, $response)
{
$cache_file = "{$this->config->data_cache_path}/anime-{$status}.json";
$transformed_cache_file = "{$this->config->data_cache_path}/anime-{$status}-transformed.json";
$cache_file = _dir($this->config->data_cache_path, "anime-{$status}.json");
$transformed_cache_file = _dir($this->config->data_cache_path, "anime-{$status}-transformed.json");
$cached = json_decode(file_get_contents($cache_file), TRUE);
$api = $response->json();
$api_data = json_decode($response->getBody(), TRUE);
if ($api !== $cached)
if ($api_data === $cached && file_exists($transformed_cache_file))
{
file_put_contents($cache_file, json_encode($api));
$transformer = new AnimeListTransformer();
$transformed = $transformer->transform_collection($api);
file_put_contents($transformed_cache_file, json_encode($transformed));
return $transformed;
return json_decode(file_get_contents($transformed_cache_file),TRUE);
}
else
{
return json_decode(file_get_contents($transformed_cache_file),TRUE);
file_put_contents($cache_file, json_encode($api_data));
$transformer = new AnimeListTransformer();
$transformed = $transformer->transform_collection($api_data);
file_put_contents($transformed_cache_file, json_encode($transformed));
return $transformed;
}
}
@ -230,7 +215,7 @@ class Anime extends API {
* @param array $array
* @return void
*/
private function sort_by_name(&$array)
protected function sort_by_name(&$array)
{
$sort = array();

View File

@ -6,12 +6,31 @@ namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\Model\API;
use Aviat\AnimeClient\Transformer\Hummingbird;
use Aviat\AnimeClient\Enum\Hummingbird\MangaReadingStatus;
/**
* Model for handling requests dealing with the manga list
*/
class Manga extends API {
const READING = 'Reading';
const PLAN_TO_READ = 'Plan to Read';
const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed';
/**
* Map API constants to display constants
* @var array
*/
protected $const_map = [
MangaReadingStatus::READING => self::READING,
MangaReadingStatus::PLAN_TO_READ => self::PLAN_TO_READ,
MangaReadingStatus::ON_HOLD => self::ON_HOLD,
MangaReadingStatus::DROPPED => self::DROPPED,
MangaReadingStatus::COMPLETED => self::COMPLETED
];
/**
* The base url for api requests
* @var string
@ -44,9 +63,9 @@ class Manga extends API {
*/
public function get_all_lists()
{
$data = $this->_get_list();
$data = $this->_get_list_from_api();
foreach ($data as $key => &$val)
foreach($data as $key => &$val)
{
$this->sort_by_name($val);
}
@ -62,24 +81,14 @@ class Manga extends API {
*/
public function get_list($status)
{
$data = $this->_get_list($status);
$data = $this->_get_list_from_api($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")
private function _get_list_from_api($status="All")
{
$errorHandler = $this->container->get('error-handler');
$cache_file = _dir($this->config->data_cache_path, 'manga.json');
$config = [
'query' => [
@ -89,81 +98,70 @@ class Manga extends API {
];
$response = $this->client->get('manga_library_entries', $config);
$data = $this->_check_cache($status, $response);
$output = $this->map_by_status($data);
$errorHandler->addDataTable('response', (array)$response);
return (array_key_exists($status, $output)) ? $output[$status] : $output;
}
if ($response->getStatusCode() != 200)
/**
* Check the status of the cache and return the appropriate response
*
* @param string $status
* @param \GuzzleHttp\Message\Response $response
* @return array
*/
private function _check_cache($status, $response)
{
// Bail out early if there isn't any manga data
$api_data = json_decode($response->getBody(), TRUE);
if ( ! array_key_exists('manga', $api_data)) return [];
$cache_file = _dir($this->config->data_cache_path, 'manga.json');
$transformed_cache_file = _dir($this->config->data_cache_path, 'manga-transformed.json');
$cached_data = json_decode(file_get_contents($cache_file), TRUE);
if ($cached_data === $api_data && file_exists($transformed_cache_file))
{
if ( ! file_exists($cache_file))
{
throw new DomainException($response->getEffectiveUrl());
}
else
{
$raw_data = json_decode(file_get_contents($cache_file), TRUE);
}
return json_decode(file_get_contents($transformed_cache_file), TRUE);
}
else
{
// Reorganize data to be more usable
$raw_data = $response->json();
file_put_contents($cache_file, json_encode($api_data));
// 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));
$zippered_data = $this->zipper_lists($api_data);
$transformer = new Hummingbird\MangaListTransformer();
$transformed_data = $transformer->transform_collection($zippered_data);
file_put_contents($transformed_cache_file, json_encode($transformed_data));
return $transformed_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' => [],
/**
* Map transformed anime data to be organized by reading status
*
* @param array $data
* @return array
*/
private function map_by_status($data)
{
$output = [
self::READING => [],
self::PLAN_TO_READ => [],
self::ON_HOLD => [],
self::DROPPED => [],
self::COMPLETED => [],
];
// Massage the two lists into one
$manga_data = $this->zipper_lists($raw_data);
// Filter data by status
foreach($manga_data as &$entry)
foreach($data as &$entry)
{
// 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;
}
$entry['manga']['image'] = $this->get_cached_image($entry['manga']['image'], $entry['manga']['slug'], 'manga');
$key = $this->const_map[$entry['reading_status']];
$output[$key][] = $entry;
}
return (array_key_exists($status, $data)) ? $data[$status] : $data;
return $output;
}
/**
@ -189,10 +187,10 @@ class Manga extends API {
foreach($array as $key => $item)
{
$sort[$key] = $item['manga']['romaji_title'];
$sort[$key] = $item['manga']['title'];
}
array_multisort($sort, SORT_ASC, $array);
}
}
// End of MangaModel.php
// End of MangaModel.php

View File

@ -0,0 +1,21 @@
<?php
namespace Aviat\Ion;
use Aviat\Ion\Type\ArrayType;
trait ArrayWrapper {
/**
* Convenience method for wrapping an array
* with the array type class
*
* @param array $arr
* @return ArrayType
*/
public function arr(array $arr)
{
return new ArrayType($arr);
}
}
// End of ArrayWrapper.php

View File

@ -2,7 +2,7 @@
namespace Aviat\Ion;
use Stringy\Stringy as S;
use Aviat\Ion\Type\StringType;
trait StringWrapper {
@ -10,11 +10,11 @@ trait StringWrapper {
* Wrap the String in the Stringy class
*
* @param string $str
* @return Stringy\Stringy
* @return StringType
*/
public function string($str)
{
return S::create($str);
return StringType::create($str);
}
}
// End of StringWrapper.php

View File

@ -0,0 +1,142 @@
<?php
namespace Aviat\Ion\Type;
/**
* Wrapper class for native array methods for convenience
*/
class ArrayType {
/**
* The current array
*
* @var array
*/
protected $arr;
/**
* Map generated methods to their native implementations
*
* @var array
*/
protected $native_methods = [
'chunk' => 'array_chunk',
'pluck' => 'array_column',
'assoc_diff' => 'array_diff_assoc',
'key_diff' => 'array_diff_key',
'diff' => 'array_diff',
'filter' => 'array_filter',
'flip' => 'array_flip',
'intersect' => 'array_intersect',
'has_key' => 'array_key_exists',
'keys' => 'array_keys',
'merge' => 'array_merge',
'pad' => 'array_pad',
'pop' => 'array_pop',
'product' => 'array_product',
'push' => 'array_push',
'random' => 'array_rand',
'reduce' => 'array_reduce',
'reverse' => 'array_reverse',
'shift' => 'array_shift',
'sum' => 'array_sum',
'unique' => 'array_unique',
'unshift' => 'array_unshift',
'values' => 'array_values',
];
/**
* Native methods that modify the passed in array
*
* @var array
*/
protected $native_in_place_methods = [
'shuffle' => 'shuffle',
];
/**
* Create an ArrayType wrapper class
*
* @param array $arr
*/
public function __construct(Array $arr)
{
$this->arr =& $arr;
}
/**
* Call one of the dynamically created methods
*
* @param string $method
* @param array $args
* @return mixed
*/
public function __call($method, $args)
{
// Simple mapping for the majority of methods
if (array_key_exists($method, $this->native_methods))
{
$func = $this->native_methods[$method];
// Set the current array as the first argument of the method
array_unshift($args, $this->arr);
return call_user_func_array($func, $args);
}
// Mapping for in-place methods
if (array_key_exists($method, $this->native_in_place_methods))
{
$func = $this->native_in_place_methods[$method];
$func($this->arr);
return $this->arr;
}
}
/**
* Fill an array with the specified value
*
* @param int $start_index
* @param int $num
* @param mixed $value
* @return array
*/
public function fill($start_index, $num, $value)
{
return array_fill($start_index, $num, $value);
}
/**
* Call a callback on each item of the array
*
* @param callable $callback
* @return array
*/
public function map(callable $callback)
{
return array_map($callback, $this->arr);
}
/**
* Find an array key by its associated value
*
* @param mixed $value
* @param bool $strict
* @return string
*/
public function search($value, $strict=FALSE)
{
return array_search($value, $this->arr, $strict);
}
/**
* Determine if the array has the passed value
*
* @param mixed $value
* @param bool $strict
* @return bool
*/
public function has($value, $strict=FALSE)
{
return in_array($value, $this->arr, $strict);
}
}
// End of ArrayType.php

View File

@ -0,0 +1,10 @@
<?php
namespace Aviat\Ion\Type;
use Stringy\Stringy;
class StringType extends Stringy {
}
// End of StringType.php

View File

@ -2,19 +2,27 @@
namespace Aviat\Ion\View;
use Aura\Html\HelperLocatorFactory;
use Aviat\Ion\View\HttpView;
use Aviat\Ion\Di\ContainerInterface;
class HtmlView extends HttpView {
/**
* HTML generator/escaper helper
*
* @var Aura\Html\HelperLocator
*/
protected $helper;
/**
* Create the Html View
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->helper = (new HelperLocatorFactory)->newInstance();
$this->helper = $container->get('html-helper');
}
/**

View File

@ -0,0 +1,54 @@
<?php
class ArrayTypeTest extends AnimeClient_TestCase {
use Aviat\Ion\ArrayWrapper;
public function setUp()
{
parent::setUp();
}
public function testMerge()
{
$obj = $this->arr([1, 3, 5, 7]);
$even_array = [2, 4, 6, 8];
$expected = [1, 3, 5, 7, 2, 4, 6, 8];
$actual = $obj->merge($even_array);
$this->assertEquals($expected, $actual);
}
public function testShuffle()
{
$original = [1, 2, 3, 4];
$test = [1, 2, 3, 4];
$obj = $this->arr($test);
$actual = $obj->shuffle();
$this->assertNotEquals($actual, $original);
$this->assertTrue(is_array($actual));
}
public function testHas()
{
$obj = $this->arr([1, 2, 6, 8, 11]);
$this->assertTrue($obj->has(8));
$this->assertFalse($obj->has(8745));
}
public function testSearch()
{
$obj = $this->arr([1, 2, 5, 7, 47]);
$actual = $obj->search(47);
$this->assertEquals(4, $actual);
}
public function testFill()
{
$obj = $this->arr([]);
$expected = ['?', '?', '?'];
$actual = $obj->fill(0, 3, '?');
$this->assertEquals($actual, $expected);
}
}