Big Work in progress commit
Some checks failed
timw4mail/HummingBirdAnimeClient/pipeline/head There was a failure building this commit

This commit is contained in:
Timothy Warren 2020-04-21 19:22:56 -04:00
parent 17fb2b4db4
commit da570d5167
28 changed files with 601 additions and 92 deletions

View File

@ -193,6 +193,16 @@ $routes = [
'username' => '.*?' 'username' => '.*?'
] ]
], ],
'anime_history' => [
'controller' => 'history',
'path' => '/history/anime',
'action' => 'anime',
],
'manga_history' => [
'controller' => 'history',
'path' => '/history/manga',
'action' => 'manga',
],
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Default / Shared routes // Default / Shared routes
// --------------------------------------------------------------------- // ---------------------------------------------------------------------

View File

@ -0,0 +1,20 @@
<main class="details fixed">
<?php if (empty($items)): ?>
<h3>No recent watch history.</h3>
<?php else: ?>
<section>
<?php foreach ($items as $name => $item): ?>
<article class="flex flex-no-wrap flex-justify-start">
<section class="flex-self-center history-img"><?= $helper->picture(
$item['coverImg'],
'jpg',
['width' => '110px', 'height' => '156px'],
['width' => '110px', 'height' => '156px']
) ?></section>
<section class="flex-self-center"><?= $item['action'] ?></section>
</article>
<?php endforeach ?>
</section>
<pre><?= print_r($items, TRUE) ?></pre>
<?php endif ?>
</main>

View File

@ -5,8 +5,8 @@ namespace Aviat\AnimeClient;
$whose = $config->get('whose_list') . "'s "; $whose = $config->get('whose_list') . "'s ";
$lastSegment = $urlGenerator->lastSegment(); $lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : ''; $extraSegment = $lastSegment === 'list' ? '/list' : '';
$hasAnime = stripos($_SERVER['REQUEST_URI'], 'anime') !== FALSE; $hasAnime = stripos($_SERVER['REQUEST_URI'], 'anime') === 1;
$hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE; $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') === 1;
?> ?>
<div id="main-nav" class="flex flex-align-end flex-wrap"> <div id="main-nav" class="flex flex-align-end flex-wrap">

View File

@ -888,6 +888,11 @@ aside picture, aside img {
filter: drop-shadow(0 -1px 4px #fff); filter: drop-shadow(0 -1px 4px #fff);
} }
.history-img {
width: 110px;
height: 156px;
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
Settings Form Settings Form
-----------------------------------------------------------------------------*/ -----------------------------------------------------------------------------*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use function in_array;
/** /**
* Class encapsulating Json API data structure for a request or response * Class encapsulating Json API data structure for a request or response
*/ */
@ -105,7 +107,7 @@ final class JsonAPI {
$relationship =& $item['relationships'][$relType]; $relationship =& $item['relationships'][$relType];
unset($relationship['data']); unset($relationship['data']);
if (\in_array($relType, $singular, TRUE)) if (in_array($relType, $singular, TRUE))
{ {
$relationship = $included[$dataType][$idKey]; $relationship = $included[$dataType][$idKey];
continue; continue;
@ -202,11 +204,11 @@ final class JsonAPI {
{ {
foreach($items as $id => $item) foreach($items as $id => $item)
{ {
if (array_key_exists('relationships', $item) && \is_array($item['relationships'])) if (array_key_exists('relationships', $item) && is_array($item['relationships']))
{ {
foreach($item['relationships'] as $relType => $props) foreach($item['relationships'] as $relType => $props)
{ {
if (array_key_exists('data', $props) && \is_array($props['data']) && array_key_exists('id', $props['data'])) if (array_key_exists('data', $props) && is_array($props['data']) && array_key_exists('id', $props['data']))
{ {
$idKey = $props['data']['id']; $idKey = $props['data']['id'];
$dataType = $props['data']['type']; $dataType = $props['data']['type'];
@ -340,7 +342,7 @@ final class JsonAPI {
foreach ($data['data'] as $item) foreach ($data['data'] as $item)
{ {
if (\is_array($item) && array_key_exists('id', $item)) if (is_array($item) && array_key_exists('id', $item))
{ {
$organized[$key][] = $item['id']; $organized[$key][] = $item['id'];
} }

View File

@ -164,9 +164,7 @@ final class Kitsu {
]; ];
} }
usort($links, function ($a, $b) { usort($links, fn ($a, $b) => $a['meta']['name'] <=> $b['meta']['name']);
return $a['meta']['name'] <=> $b['meta']['name'];
});
return $links; return $links;
} }

View File

@ -91,9 +91,9 @@ final class Auth {
$cacheItem->save(); $cacheItem->save();
// Set the token expiration in the cache // Set the token expiration in the cache
$expire_time = $auth['created_at'] + $auth['expires_in']; $expireTime = $auth['created_at'] + $auth['expires_in'];
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY); $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY);
$cacheItem->set($expire_time); $cacheItem->set($expireTime);
$cacheItem->save(); $cacheItem->save();
// Set the refresh token in the cache // Set the refresh token in the cache
@ -103,7 +103,7 @@ final class Auth {
// Set the session values // Set the session values
$this->segment->set('auth_token', $auth['access_token']); $this->segment->set('auth_token', $auth['access_token']);
$this->segment->set('auth_token_expires', $expire_time); $this->segment->set('auth_token_expires', $expireTime);
$this->segment->set('refresh_token', $auth['refresh_token']); $this->segment->set('refresh_token', $auth['refresh_token']);
return TRUE; return TRUE;

View File

@ -176,7 +176,7 @@ trait KitsuTrait {
$logger->warning('Non 200 response for api call', (array)$response); $logger->warning('Non 200 response for api call', (array)$response);
} }
throw new FailedResponseException('Failed to get the proper response from the API'); // throw new FailedResponseException('Failed to get the proper response from the API');
} }
try try

View File

@ -31,6 +31,7 @@ use Aviat\AnimeClient\API\Enum\{
}; };
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\API\Kitsu\Transformer\{ use Aviat\AnimeClient\API\Kitsu\Transformer\{
AnimeHistoryTransformer,
AnimeTransformer, AnimeTransformer,
AnimeListTransformer, AnimeListTransformer,
MangaTransformer, MangaTransformer,
@ -173,6 +174,38 @@ final class Model {
return FALSE; return FALSE;
} }
/**
* Retrieve the data for the anime watch history page
*
* @return array
* @throws InvalidArgumentException
* @throws Throwable
*/
public function getAnimeHistory(): array
{
$raw = $this->getRawHistoryList('anime');
$organized = JsonAPI::organizeData($raw);
$transformer = new AnimeHistoryTransformer();
$transformer->setContainer($this->getContainer());
return $transformer->transform($organized);
}
/**
* Retrieve the data for the manga read history page
*
* @return array
* @throws InvalidArgumentException
* @throws Throwable
*/
public function getMangaHistory(): array
{
$raw = $this->getRawHistoryList('manga');
return JsonAPI::organizeData($raw);
}
/** /**
* Get the userid for a username from Kitsu * Get the userid for a username from Kitsu
* *
@ -455,7 +488,7 @@ final class Model {
'query' => [ 'query' => [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername(), 'user_id' => $this->getUserIdByUsername(),
'media_type' => 'Anime' 'kind' => 'anime'
], ],
'page' => [ 'page' => [
'limit' => 1 'limit' => 1
@ -584,7 +617,7 @@ final class Model {
$defaultOptions = [ $defaultOptions = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Anime' 'kind' => 'anime'
], ],
'page' => [ 'page' => [
'offset' => $offset, 'offset' => $offset,
@ -610,7 +643,7 @@ final class Model {
$options = [ $options = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Anime', 'kind' => 'anime',
'status' => $status, 'status' => $status,
], ],
'include' => 'media,media.categories,media.mappings,anime.streamingLinks', 'include' => 'media,media.categories,media.mappings,anime.streamingLinks',
@ -669,7 +702,7 @@ final class Model {
'query' => [ 'query' => [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Manga', 'kind' => 'manga',
'status' => $status, 'status' => $status,
], ],
'include' => 'media,media.categories,media.mappings', 'include' => 'media,media.categories,media.mappings',
@ -724,7 +757,7 @@ final class Model {
'query' => [ 'query' => [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername(), 'user_id' => $this->getUserIdByUsername(),
'media_type' => 'Manga' 'kind' => 'manga'
], ],
'page' => [ 'page' => [
'limit' => 1 'limit' => 1
@ -817,7 +850,7 @@ final class Model {
$defaultOptions = [ $defaultOptions = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Manga' 'kind' => 'manga'
], ],
'page' => [ 'page' => [
'offset' => $offset, 'offset' => $offset,
@ -942,6 +975,71 @@ final class Model {
return $this->listItem->delete($id); return $this->listItem->delete($id);
} }
/**
* Get the aggregated pages of anime or manga history
*
* @param string $type
* @param int $entries
* @return array
* @throws InvalidArgumentException
* @throws Throwable
*/
protected function getRawHistoryList(string $type = 'anime', int $entries = 60): array
{
$size = 20;
$pages = ceil($entries / $size);
$requester = new ParallelAPIRequest();
// Set up requests
for ($i = 0; $i < $pages; $i++)
{
$offset = $i * $size;
$requester->addRequest($this->getRawHistoryPage($type, $offset, $size));
}
$responses = $requester->makeRequests();
$output = [];
foreach($responses as $response)
{
$data = Json::decode($response);
$output[] = $data;
}
return array_merge_recursive(...$output);
}
/**
* Retrieve one page of the anime or manga history
*
* @param string $type
* @param int $offset
* @param int $limit
* @return Request
* @throws InvalidArgumentException
*/
protected function getRawHistoryPage(string $type, int $offset, int $limit = 20): Request
{
return $this->setUpRequest('GET', 'library-events', [
'query' => [
'filter' => [
'kind' => 'progressed,updated',
'userId' => $this->getUserIdByUsername($this->getUsername()),
],
'page' => [
'offset' => $offset,
'limit' => $limit,
],
'fields' => ($type === 'anime')
? ['anime' => 'canonicalTitle,titles,slug,posterImage']
: ['manga' => 'canonicalTitle,titles,slug,posterImage'],
'sort' => '-updated_at',
'include' => $type,
],
]);
}
/** /**
* Get the kitsu username from config * Get the kitsu username from config
* *

View File

@ -0,0 +1,210 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\HistoryItem;
use Aviat\Ion\Di\ContainerAware;
class AnimeHistoryTransformer {
use ContainerAware;
protected array $skipList = [];
/**
* Convert raw history
*
* @param array $data
* @return array
*/
public function transform(array $data): array
{
$output = [];
foreach ($data as $id => $entry)
{
if ( ! isset($entry['relationships']['anime']))
{
continue;
}
if (in_array($id, $this->skipList, FALSE))
{
continue;
}
if ($entry['attributes']['kind'] === 'progressed')
{
$output[] = $this->transformProgress($entry);
}
else if ($entry['attributes']['kind'] === 'updated')
{
$output[] = $this->transformUpdated($entry);
}
}
return $this->aggregate($output);
}
/**
* Combine consecutive 'progressed' events
*
* @param array $singles
* @return array
*/
protected function aggregate (array $singles): array
{
$output = [];
$prevTitle = '';
$count = count($singles);
for ($i = 0; $i < $count; $i++)
{
$entry = $singles[$i];
$nextId = $i + 1;
if ($nextId < $count)
{
$entries = [];
$next = $singles[$nextId];
while (
$next['kind'] === 'progressed' &&
$next['title'] === $prevTitle
) {
$entries[] = $next;
$prevTitle = $next['title'];
if ($nextId + 1 < $count)
{
$nextId++;
$next = $singles[$nextId];
}
else
{
break;
}
}
}
if (count($entries) > 1)
{
$episodes = [];
foreach ($entries as $e)
{
$episodes[] = max($e['original']['attributes']['changedData']['progress']);
}
$firstEpisode = min($episodes);
$lastEpisode = max($episodes);
$title = $entries[0]['title'];
// Get rid of the single entry added before aggregating
// array_pop($output);
$action = (count($entries) > 3)
? "Marathoned episodes {$firstEpisode}-{$lastEpisode} of {$title}"
: "Watched episodes {$firstEpisode}-{$lastEpisode} of {$title}";
$output[] = HistoryItem::check([
'title' => $title,
'action' => $action,
'coverImg' => $entries[0]['coverImg'],
'isAggregate' => true,
'updated' => $entries[0]['updated'],
]);
// Skip the rest of the aggregate in the main loop
$i += count($entries);
$prevTitle = $title;
continue;
}
else
{
$prevTitle = $entry['title'];
$output[] = $entry;
}
}
return $output;
}
protected function transformProgress ($entry): array
{
$animeId = array_keys($entry['relationships']['anime'])[0];
$animeData = $entry['relationships']['anime'][$animeId]['attributes'];
$title = $this->linkTitle($animeData);
$imgUrl = 'images/anime/' . $animeId . '.webp';
$episode = max($entry['attributes']['changedData']['progress']);
return HistoryItem::check([
'action' => "Watched episode {$episode} of {$title}",
'coverImg' => $imgUrl,
'kind' => 'progressed',
'original' => $entry,
'title' => $title,
'updated' => $entry['attributes']['updatedAt'],
]);
}
protected function transformUpdated($entry): array
{
$animeId = array_keys($entry['relationships']['anime'])[0];
$animeData = $entry['relationships']['anime'][$animeId]['attributes'];
$title = $this->linkTitle($animeData);
$imgUrl = 'images/anime/' . $animeId . '.webp';
$kind = array_key_first($entry['attributes']['changedData']);
if ($kind === 'status')
{
$status = array_pop($entry['attributes']['changedData']['status']);
$statusName = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
if ($statusName === 'Completed')
{
return HistoryItem::check([
'action' => "Completed {$title}",
'coverImg' => $imgUrl,
'kind' => 'updated',
'original' => $entry,
'title' => $title,
'updated' => $entry['attributes']['updatedAt'],
]);
}
return HistoryItem::check([
'action' => "Set status of {$title} to {$statusName}",
'coverImg' => $imgUrl,
'kind' => 'updated',
'original' => $entry,
'title' => $title,
'updated' => $entry['attributes']['updatedAt'],
]);
}
return $entry;
}
protected function linkTitle (array $animeData): string
{
$url = '/anime/details/' . $animeData['slug'];
$helper = $this->getContainer()->get('html-helper');
return $helper->a($url, $animeData['canonicalTitle'], ['id' => $animeData['slug']]);
}
}

View File

@ -213,9 +213,9 @@ function checkFolderPermissions(ConfigInterface $config): array
/** /**
* Get an API Client, with better defaults * Get an API Client, with better defaults
* *
* @return DefaultClient * @return HttpClient
*/ */
function getApiClient () function getApiClient (): HttpClient
{ {
static $client; static $client;
@ -290,7 +290,7 @@ function getLocalImg ($kitsuUrl, $webp = TRUE): string
* @param int $height * @param int $height
* @param string $text * @param string $text
*/ */
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable'): void function createPlaceholderImage ($path, ?int $width, ?int $height, $text = 'Image Unavailable'): void
{ {
$width = $width ?? 200; $width = $width ?? 200;
$height = $height ?? 200; $height = $height ?? 200;

View File

@ -30,6 +30,7 @@ use Aviat\Ion\Json;
use InvalidArgumentException; use InvalidArgumentException;
use Throwable; use Throwable;
use TypeError;
/** /**
* Controller for Anime-related pages * Controller for Anime-related pages
@ -338,7 +339,7 @@ final class Anime extends BaseController {
'data' => $data, 'data' => $data,
]); ]);
} }
catch (\TypeError $e) catch (TypeError $e)
{ {
$this->notFound( $this->notFound(
$this->config->get('whose_list') . $this->config->get('whose_list') .
@ -348,15 +349,5 @@ final class Anime extends BaseController {
); );
} }
} }
/**
* Find anime matching the selected genre
*
* @param string $genre
*/
public function genre(string $genre): void
{
// @TODO: implement
}
} }
// End of AnimeController.php // End of AnimeController.php

View File

@ -0,0 +1,84 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Model\Manga as MangaModel;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
/**
* Controller for Anime-related pages
*/
final class History extends BaseController {
/**
* The anime list model
* @var AnimeModel
*/
protected AnimeModel $animeModel;
/**
* The manga list model
* @var MangaModel
*/
protected MangaModel $mangaModel;
/**
* Constructor
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->animeModel = $container->get('anime-model');
$this->mangaModel = $container->get('manga-model');
}
public function anime(): void
{
// $this->outputJSON($this->animeModel->getHistory());
// return;
$this->outputHTML('history/anime', [
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Anime List",
'Anime',
'Watching History'
),
'items' => $this->animeModel->getHistory(),
]);
}
public function manga(): void
{
$this->outputJSON($this->mangaModel->getHistory());
return;
$this->outputHTML('history/manga', [
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Manga List",
'Manga',
'Reading History'
),
'items' => $this->mangaModel->getHistory(),
]);
}
}

View File

@ -337,14 +337,11 @@ final class Manga extends Controller {
]); ]);
} }
/** public function history(): void
* Find manga matching the selected genre
*
* @param string $genre
*/
public function genre(string $genre): void
{ {
// @TODO: implement $data = $this->model->getHistory();
$this->outputJSON($data);
} }
} }
// End of MangaController.php // End of MangaController.php

View File

@ -29,6 +29,7 @@ use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use Throwable; use Throwable;
use function is_array;
/** /**
* Model for handling requests dealing with the anime list * Model for handling requests dealing with the anime list
@ -128,6 +129,16 @@ class Anime extends API {
return $this->kitsuModel->getAnimeById($animeId); return $this->kitsuModel->getAnimeById($animeId);
} }
/**
* Get recent watch history
*
* @return array
*/
public function getHistory(): array
{
return $this->kitsuModel->getAnimeHistory();
}
/** /**
* Search for anime by name * Search for anime by name
* *
@ -151,7 +162,7 @@ class Anime extends API {
$item = $this->kitsuModel->getListItem($itemId); $item = $this->kitsuModel->getListItem($itemId);
$array = $item->toArray(); $array = $item->toArray();
if (\is_array($array['notes'])) if (is_array($array['notes']))
{ {
$array['notes'] = ''; $array['notes'] = '';
} }

View File

@ -367,7 +367,7 @@ final class AnimeCollection extends Collection {
} }
catch (PDOException $e) {} catch (PDOException $e) {}
$this->db->reset_query(); $this->db->resetQuery();
return $output; return $output;
} }
@ -446,7 +446,7 @@ final class AnimeCollection extends Collection {
try try
{ {
$this->db->insert_batch('genres', $insert); $this->db->insertBatch('genres', $insert);
} }
catch (PDOException $e) catch (PDOException $e)
{ {
@ -486,7 +486,7 @@ final class AnimeCollection extends Collection {
$genres[$genre['id']] = $genre['genre']; $genres[$genre['id']] = $genre['genre'];
} }
$this->db->reset_query(); $this->db->resetQuery();
return $genres; return $genres;
} }
@ -509,13 +509,14 @@ final class AnimeCollection extends Collection {
if (array_key_exists($link['hummingbird_id'], $links)) if (array_key_exists($link['hummingbird_id'], $links))
{ {
$links[$link['hummingbird_id']][] = $link['genre_id']; $links[$link['hummingbird_id']][] = $link['genre_id'];
} else }
else
{ {
$links[$link['hummingbird_id']] = [$link['genre_id']]; $links[$link['hummingbird_id']] = [$link['genre_id']];
} }
} }
$this->db->reset_query(); $this->db->resetQuery();
return $links; return $links;
} }

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use PDOException; use PDOException;
use Query\QueryBuilder; use Query\QueryBuilderInterface;
use function Query; use function Query;
/** /**
@ -29,9 +29,9 @@ class Collection extends DB {
/** /**
* The query builder object * The query builder object
* @var QueryBuilder * @var QueryBuilderInterface
*/ */
protected QueryBuilder $db; protected QueryBuilderInterface $db;
/** /**
* Whether the database is valid for querying * Whether the database is valid for querying

View File

@ -232,7 +232,7 @@ class Manga extends API {
} }
/** /**
* Search for anime by name * Search for manga by name
* *
* @param string $name * @param string $name
* @return array * @return array
@ -242,6 +242,16 @@ class Manga extends API {
return $this->kitsuModel->search('manga', $name); return $this->kitsuModel->search('manga', $name);
} }
/**
* Get recent reading history
*
* @return array
*/
public function getHistory(): array
{
return $this->kitsuModel->getMangaHistory();
}
/** /**
* Map transformed anime data to be organized by reading status * Map transformed anime data to be organized by reading status
* *

View File

@ -31,6 +31,24 @@ abstract class AbstractType implements ArrayAccess, Countable {
return new static($properties); return new static($properties);
} }
/**
* Check the shape of the object, and return the array equivalent
*
* @param array $data
* @return array|null
*/
final public static function check($data = []): ?array
{
$currentClass = static::class;
if (get_parent_class($currentClass) !== FALSE)
{
return (new $currentClass($data))->toArray();
}
return NULL;
}
/** /**
* Sets the properties by using the constructor * Sets the properties by using the constructor
* *
@ -61,7 +79,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $name * @param $name
* @return bool * @return bool
*/ */
public function __isset($name): bool final public function __isset($name): bool
{ {
return property_exists($this, $name) && isset($this->$name); return property_exists($this, $name) && isset($this->$name);
} }
@ -73,7 +91,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param mixed $value * @param mixed $value
* @return void * @return void
*/ */
public function __set($name, $value): void final public function __set($name, $value): void
{ {
$setterMethod = 'set' . ucfirst($name); $setterMethod = 'set' . ucfirst($name);
@ -99,7 +117,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param string $name * @param string $name
* @return mixed * @return mixed
*/ */
public function __get($name) final public function __get($name)
{ {
// Be a bit more lenient here, so that you can easily typecast missing // Be a bit more lenient here, so that you can easily typecast missing
// values to reasonable defaults, and not have to resort to array indexes // values to reasonable defaults, and not have to resort to array indexes
@ -122,7 +140,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $offset * @param $offset
* @return bool * @return bool
*/ */
public function offsetExists($offset): bool final public function offsetExists($offset): bool
{ {
return $this->__isset($offset); return $this->__isset($offset);
} }
@ -133,7 +151,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $offset * @param $offset
* @return mixed * @return mixed
*/ */
public function offsetGet($offset) final public function offsetGet($offset)
{ {
return $this->__get($offset); return $this->__get($offset);
} }
@ -144,7 +162,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $offset * @param $offset
* @param $value * @param $value
*/ */
public function offsetSet($offset, $value): void final public function offsetSet($offset, $value): void
{ {
$this->__set($offset, $value); $this->__set($offset, $value);
} }
@ -154,7 +172,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* *
* @param $offset * @param $offset
*/ */
public function offsetUnset($offset): void final public function offsetUnset($offset): void
{ {
if ($this->offsetExists($offset)) if ($this->offsetExists($offset))
{ {
@ -167,7 +185,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* *
* @return int * @return int
*/ */
public function count(): int final public function count(): int
{ {
$keys = array_keys($this->toArray()); $keys = array_keys($this->toArray());
return count($keys); return count($keys);
@ -179,7 +197,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param mixed $parent * @param mixed $parent
* @return mixed * @return mixed
*/ */
public function toArray($parent = null) final public function toArray($parent = null)
{ {
$object = $parent ?? $this; $object = $parent ?? $this;
@ -205,7 +223,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* *
* @return bool * @return bool
*/ */
public function isEmpty(): bool final public function isEmpty(): bool
{ {
foreach ($this as $value) foreach ($this as $value)
{ {

View File

@ -25,85 +25,85 @@ class Anime extends AbstractType {
/** /**
* @var string * @var string
*/ */
public $age_rating; public string $age_rating = '';
/** /**
* @var string * @var string
*/ */
public $age_rating_guide; public ?string $age_rating_guide = '';
/** /**
* @var string * @var string
*/ */
public $cover_image; public string $cover_image = '';
/** /**
* @var string|int * @var string|int
*/ */
public $episode_count; public ?int $episode_count = 13;
/** /**
* @var string|int * @var string|int
*/ */
public $episode_length; public ?int $episode_length = 24;
/** /**
* @var array * @var array
*/ */
public $genres; public array $genres = [];
/** /**
* @var string * @var string
*/ */
public $id; public string $id = '';
/** /**
* @var array * @var array
*/ */
public $included; public array $included = [];
/** /**
* @var string * @var string
*/ */
public $show_type; public string $show_type = '';
/** /**
* @var string * @var string
*/ */
public $slug; public string $slug = '';
/** /**
* @var AnimeAiringStatus::NOT_YET_AIRED | AnimeAiringStatus::AIRING | AnimeAiringStatus::FINISHED_AIRING * @var AnimeAiringStatus
*/ */
public $status; public string $status = AnimeAiringStatus::FINISHED_AIRING;
/** /**
* @var array * @var array
*/ */
public $streaming_links; public ?array $streaming_links = [];
/** /**
* @var string * @var string
*/ */
public $synopsis; public string $synopsis = '';
/** /**
* @var string * @var string
*/ */
public $title; public string $title = '';
/** /**
* @var array * @var array
*/ */
public $titles; public array $titles = [];
/** /**
* @var string * @var string
*/ */
public $trailer_id; public ?string $trailer_id = '';
/** /**
* @var string * @var string
*/ */
public $url; public string $url = '';
} }

View File

@ -0,0 +1,51 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
class HistoryItem extends AbstractType {
/**
* @var string Title of the anime/manga
*/
public string $title = '';
/**
* @var string The url of the cover image
*/
public string $coverImg = '';
/**
* @var string The type of action done
*/
public string $action = '';
/**
* @var bool Is this item a combination of items?
*/
public bool $isAggregate = FALSE;
/**
* @var string The kind of history event
*/
public string $kind = '';
/**
* @var string When the item was last updated
*/
public string $updated = '';
public $original;
}

View File

@ -20,6 +20,8 @@ use Psr\Http\Message\ResponseInterface;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Exception\DoubleRenderException; use Aviat\Ion\Exception\DoubleRenderException;
use InvalidArgumentException;
use RuntimeException;
/** /**
* Base view response class * Base view response class
@ -91,7 +93,7 @@ abstract class View
* *
* @param string $name * @param string $name
* @param string|string[] $value * @param string|string[] $value
* @throws \InvalidArgumentException * @throws InvalidArgumentException
* @return ViewInterface * @return ViewInterface
*/ */
public function addHeader(string $name, $value): ViewInterface public function addHeader(string $name, $value): ViewInterface
@ -104,8 +106,8 @@ abstract class View
* Set the output string * Set the output string
* *
* @param mixed $string * @param mixed $string
* @throws \InvalidArgumentException * @throws InvalidArgumentException
* @throws \RuntimeException * @throws RuntimeException
* @return ViewInterface * @return ViewInterface
*/ */
public function setOutput($string): ViewInterface public function setOutput($string): ViewInterface
@ -119,8 +121,8 @@ abstract class View
* Append additional output. * Append additional output.
* *
* @param string $string * @param string $string
* @throws \InvalidArgumentException * @throws InvalidArgumentException
* @throws \RuntimeException * @throws RuntimeException
* @return ViewInterface * @return ViewInterface
*/ */
public function appendOutput(string $string): ViewInterface public function appendOutput(string $string): ViewInterface

View File

@ -31,14 +31,14 @@ class HtmlView extends HttpView {
* *
* @var HelperLocator * @var HelperLocator
*/ */
protected $helper; protected HelperLocator $helper;
/** /**
* Response mime type * Response mime type
* *
* @var string * @var string
*/ */
protected $contentType = 'text/html'; protected string $contentType = 'text/html';
/** /**
* Create the Html View * Create the Html View
@ -73,7 +73,7 @@ class HtmlView extends HttpView {
// Very basic html minify, that won't affect content between html tags // Very basic html minify, that won't affect content between html tags
$buffer = preg_replace('/>\s+</', '> <', $buffer); // $buffer = preg_replace('/>\s+</', '> <', $buffer);
return $buffer; return $buffer;
} }

View File

@ -32,7 +32,7 @@ class HttpView extends BaseView {
* *
* @var string * @var string
*/ */
protected $contentType = ''; protected string $contentType = '';
/** /**
* Do a redirect * Do a redirect

View File

@ -19,6 +19,7 @@ namespace Aviat\Ion\View;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use Aviat\Ion\JsonException; use Aviat\Ion\JsonException;
use Aviat\Ion\ViewInterface; use Aviat\Ion\ViewInterface;
use function is_string;
/** /**
* View class to serialize Json * View class to serialize Json
@ -30,7 +31,7 @@ class JsonView extends HttpView {
* *
* @var string * @var string
*/ */
protected $contentType = 'application/json'; protected string $contentType = 'application/json';
/** /**
* Set the output string * Set the output string
@ -43,7 +44,7 @@ class JsonView extends HttpView {
*/ */
public function setOutput($string): ViewInterface public function setOutput($string): ViewInterface
{ {
if ( ! \is_string($string)) if ( ! is_string($string))
{ {
$string = Json::encode($string); $string = Json::encode($string);
} }