Merge remote-tracking branch 'origin/develop'
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good Details

This commit is contained in:
Timothy Warren 2023-06-27 21:18:27 -04:00
commit 73bbc569a7
131 changed files with 3489 additions and 2076 deletions

View File

@ -5,22 +5,12 @@ use PhpCsFixer\{Config, Finder};
$finder = Finder::create() $finder = Finder::create()
->in([ ->in([
__DIR__, __DIR__ . '/src',
__DIR__ . '/app', __DIR__ . '/tests',
__DIR__ . '/tools', __DIR__ . '/tools',
]) ])
->exclude([ ->exclude([
'apidocs',
'build',
'coverage',
'frontEndSrc',
'phinx',
'public',
'tools',
'tmp',
'vendor', 'vendor',
'views',
'templates',
]); ]);
return (new Config()) return (new Config())
@ -45,7 +35,7 @@ return (new Config())
'blank_line_after_opening_tag' => false, 'blank_line_after_opening_tag' => false,
'blank_line_before_statement' => [ 'blank_line_before_statement' => [
'statements' => [ 'statements' => [
'case', // 'case',
'continue', 'continue',
'declare', 'declare',
'default', 'default',
@ -128,12 +118,12 @@ return (new Config())
'noise_remaining_usages_exclude' => [], 'noise_remaining_usages_exclude' => [],
], ],
'escape_implicit_backslashes' => [ 'escape_implicit_backslashes' => [
'double_quoted' => true, 'double_quoted' => false,
'heredoc_syntax' => true, 'heredoc_syntax' => false,
'single_quoted' => false, 'single_quoted' => false,
], ],
'explicit_indirect_variable' => true, 'explicit_indirect_variable' => false,
'explicit_string_variable' => true, 'explicit_string_variable' => false,
'final_class' => false, 'final_class' => false,
'final_internal_class' => [ 'final_internal_class' => [
'annotation_exclude' => ['@no-final'], 'annotation_exclude' => ['@no-final'],
@ -167,7 +157,7 @@ return (new Config())
], ],
'group_import' => true, 'group_import' => true,
'header_comment' => false, // false by default 'header_comment' => false, // false by default
'heredoc_indentation' => ['indentation' => 'start_plus_one'], // 'heredoc_indentation' => ['indentation' => 'start_plus_one'],
'heredoc_to_nowdoc' => true, 'heredoc_to_nowdoc' => true,
'implode_call' => true, 'implode_call' => true,
'include' => true, 'include' => true,
@ -232,8 +222,7 @@ return (new Config())
'allow_unused_params' => true, 'allow_unused_params' => true,
'remove_inheritdoc' => false, 'remove_inheritdoc' => false,
], ],
'no_trailing_comma_in_list_call' => true, 'no_trailing_comma_in_singleline' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_trailing_whitespace' => true, 'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true, 'no_trailing_whitespace_in_comment' => true,
'no_trailing_whitespace_in_string' => true, 'no_trailing_whitespace_in_string' => true,
@ -270,9 +259,16 @@ return (new Config())
'ordered_class_elements' => [ 'ordered_class_elements' => [
'order' => [ 'order' => [
'use_trait', 'use_trait',
'constant', 'case',
'property', 'constant_public',
'method', 'constant_protected',
'constant_private',
'property_public',
'property_protected',
'property_private',
'construct',
'destruct',
'magic',
], ],
'sort_algorithm' => 'none', 'sort_algorithm' => 'none',
], ],

View File

@ -1,8 +1,9 @@
# Changelog # Changelog
## Version 5.2 ## Version 5.2
* Updated PHP requirement to 8 * Updated PHP requirement to 8.1
* Updated to support PHP 8.1 * Updated to support PHP 8.2
* Improve Anilist <-> Kitsu mappings to be more reliable
## Version 5.1 ## Version 5.1
* Added session check, so when coming back to a page, if the session is expired, the page will refresh. * Added session check, so when coming back to a page, if the session is expired, the page will refresh.

0
app/logs/.gitkeep Normal file → Executable file
View File

View File

@ -1,6 +1,6 @@
<?php <?php
use Aviat\AnimeClient\Kitsu; use function Aviat\AnimeClient\friendlyTime;
?> ?>
<main class="details fixed"> <main class="details fixed">
@ -38,14 +38,14 @@ use Aviat\AnimeClient\Kitsu;
<?php if (( ! empty($data['episode_length'])) && $data['episode_count'] !== 1): ?> <?php if (( ! empty($data['episode_length'])) && $data['episode_count'] !== 1): ?>
<tr> <tr>
<td>Episode Length</td> <td>Episode Length</td>
<td><?= Kitsu::friendlyTime($data['episode_length']) ?></td> <td><?= friendlyTime($data['episode_length']) ?></td>
</tr> </tr>
<?php endif ?> <?php endif ?>
<?php if (isset($data['total_length'], $data['episode_count']) && $data['total_length'] > 0): ?> <?php if (isset($data['total_length'], $data['episode_count']) && $data['total_length'] > 0): ?>
<tr> <tr>
<td>Total Length</td> <td>Total Length</td>
<td><?= Kitsu::friendlyTime($data['total_length']) ?></td> <td><?= friendlyTime($data['total_length']) ?></td>
</tr> </tr>
<?php endif ?> <?php endif ?>

View File

@ -26,7 +26,7 @@ use Aviat\AnimeClient\Kitsu;
<br /> <br />
<hr /> <hr />
<div class="description"> <div class="description">
<p><?= str_replace("\n", '</p><p>', $data['description']) ?></p> <p><?= nl2br($data['description']) ?></p>
</div> </div>
</div> </div>
</section> </section>

View File

@ -8,6 +8,9 @@
<?php foreach ($data['names'] as $name): ?> <?php foreach ($data['names'] as $name): ?>
<h3><?= $name ?></h3> <h3><?= $name ?></h3>
<?php endforeach ?> <?php endforeach ?>
<?php if ( ! empty($data['birthday'])): ?>
<h4><?= $data['birthday'] ?></h4>
<?php endif ?>
<br /> <br />
<hr /> <hr />
<div class="description"> <div class="description">

View File

@ -3,40 +3,58 @@ use Aviat\AnimeClient\Kitsu;
?> ?>
<main class="user-page details"> <main class="user-page details">
<h2 class="toph"> <h2 class="toph">
About
<?= $helper->a( <?= $helper->a(
"https://kitsu.io/users/{$data['slug']}", "https://kitsu.io/users/{$data['slug']}",
$data['name'], [ $data['name'], [
'title' => 'View profile on Kitsu' 'title' => 'View profile on Kitsu'
]) ])
?> ?>
</h2> </h2>
<p><?= $escape->html($data['about']) ?></p>
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<aside class="info"> <aside class="info">
<center> <table class="media-details invisible">
<?= $helper->img($data['avatar'], ['alt' => '']); ?> <tr>
</center> <?php if($data['avatar'] !== null): ?>
<td><?= $helper->img($data['avatar'], ['alt' => '', 'width' => '225']); ?></td>
<?php endif ?>
<td><?= $escape->html($data['about']) ?></td>
</tr>
</table>
<br /> <br />
<table class="media-details"> <table class="media-details">
<?php foreach ([
'joinDate' => 'Joined',
'birthday' => 'Birthday',
'gender' => 'Gender',
'location' => 'Location'
] as $key => $label): ?>
<?php if ($data[$key] !== null): ?>
<tr> <tr>
<td>Location</td> <td><?= $label ?></td>
<td><?= $data['location'] ?></td> <td><?= $data[$key] ?></td>
</tr> </tr>
<?php endif ?>
<?php endforeach; ?>
<?php if ($data['website'] !== null): ?>
<tr> <tr>
<td>Website</td> <td>Website</td>
<td><?= $helper->a($data['website'], $data['website']) ?></td> <td><?= $helper->a($data['website'], $data['website']) ?></td>
</tr> </tr>
<?php if ( ! empty($data['waifu'])): ?> <?php endif ?>
<?php if ($data['waifu']['character'] !== null): ?>
<tr> <tr>
<td><?= $escape->html($data['waifu']['label']) ?></td> <td><?= $escape->html($data['waifu']['label']) ?></td>
<td> <td>
<?php <?php
$character = $data['waifu']['character']; $character = $data['waifu']['character'];
echo $helper->a( echo $component->character(
$url->generate('character', ['slug' => $character['slug']]), $character['names']['canonical'],
$character['names']['canonical'] $url->generate('character', ['slug' => $character['slug']]),
$helper->img(Kitsu::getImage($character))
); );
?> ?>
</td> </td>
@ -75,7 +93,7 @@ use Aviat\AnimeClient\Kitsu;
$rendered[] = $component->character( $rendered[] = $component->character(
$item['names']['canonical'], $item['names']['canonical'],
$url->generate('character', ['slug' => $item['slug']]), $url->generate('character', ['slug' => $item['slug']]),
$helper->img($item['image']['original']['url']) $helper->img(Kitsu::getImage($item))
); );
} }
else else

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" stopOnFailure="false" bootstrap="../tests/bootstrap.php" beStrictAboutTestsThatDoNotTestAnything="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" stopOnFailure="false" bootstrap="../tests/bootstrap.php" beStrictAboutTestsThatDoNotTestAnything="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd">
<coverage> <coverage>
<include>
<directory suffix=".php">../src</directory>
</include>
<report> <report>
<clover outputFile="logs/clover.xml"/> <clover outputFile="logs/clover.xml"/>
<html outputDirectory="../coverage"/> <html outputDirectory="../coverage"/>
@ -14,12 +11,12 @@
<directory>../tests/AnimeClient</directory> <directory>../tests/AnimeClient</directory>
</testsuite> </testsuite>
<testsuite name="Ion"> <testsuite name="Ion">
<directory>../tests/Ion</directory> <directory>../tests/Ion</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<logging> <logging>
<junit outputFile="logs/junit.xml"/> <junit outputFile="logs/junit.xml"/>
</logging> </logging>
<php> <php>
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0"/> <server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0"/>
<server name="HTTP_HOST" value="localhost"/> <server name="HTTP_HOST" value="localhost"/>
@ -27,4 +24,9 @@
<server name="REQUEST_URI" value="/"/> <server name="REQUEST_URI" value="/"/>
<server name="REQUEST_METHOD" value="GET"/> <server name="REQUEST_METHOD" value="GET"/>
</php> </php>
<source>
<include>
<directory suffix=".php">../src</directory>
</include>
</source>
</phpunit> </phpunit>

View File

@ -30,21 +30,20 @@
"lock": false "lock": false
}, },
"require": { "require": {
"amphp/amp": "^2.5.0",
"amphp/http-client": "^4.5.0", "amphp/http-client": "^4.5.0",
"aura/html": "^2.5.0", "aura/html": "^2.5.0",
"aura/router": "^3.1.0", "aura/router": "^3.1.0",
"aura/session": "^2.1.0", "aura/session": "^2.1.0",
"aviat/banker": "^4.1.2", "aviat/banker": "^4.1.2",
"aviat/query": "^4.0.0", "aviat/query": "^4.1.0",
"ext-dom": "*", "ext-dom": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-pdo": "*", "ext-pdo": "*",
"laminas/laminas-diactoros": "^2.5.0", "laminas/laminas-diactoros": "^3.0.0",
"laminas/laminas-httphandlerrunner": "^2.1.0", "laminas/laminas-httphandlerrunner": "^2.6.1",
"maximebf/consolekit": "^1.0.3", "maximebf/consolekit": "^1.0.3",
"monolog/monolog": "^3.0.0", "monolog/monolog": "^3.0.0",
"php": ">= 8.1.0", "php": ">= 8.1.0",
@ -56,9 +55,9 @@
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.2.0", "phpstan/phpstan": "^1.2.0",
"phpunit/phpunit": "^9.5.0", "phpunit/phpunit": "^10.0.0",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"spatie/phpunit-snapshot-assertions": "^4.1.0" "spatie/phpunit-snapshot-assertions": "^5.0.1"
}, },
"scripts": { "scripts": {
"build:css": "cd public && npm run build:css && cd ..", "build:css": "cd public && npm run build:css && cd ..",

View File

@ -333,7 +333,8 @@ td.danger, td.danger:hover, td.danger:active {
.borderless th, .borderless th,
.invisible tr, .invisible tr,
.invisible td, .invisible td,
.invisible th { .invisible th,
table.invisible {
box-shadow: none; box-shadow: none;
border: 0; border: 0;
} }
@ -836,19 +837,11 @@ aside.info {
max-width: 390px; max-width: 390px;
} }
/* .fixed aside.info + article {
max-width: inherit;
} */
aside picture, aside img { aside picture, aside img {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
} }
/* aside.info + article {
max-width: 66%;
} */
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
User page styles User page styles
-----------------------------------------------------------------------------*/ -----------------------------------------------------------------------------*/

View File

@ -1,5 +1,6 @@
import _ from './anime-client.js' import _ from './anime-client.js'
import { renderSearchResults } from './template-helpers.js' import { renderSearchResults } from './template-helpers.js'
import { getNestedProperty, hasNestedProperty } from "./fns";
const search = (query, isCollection = false) => { const search = (query, isCollection = false) => {
// Show the loader // Show the loader
@ -70,6 +71,14 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
} }
}; };
const displayMessage = (type, message) => {
_.hide('#loading-shadow');
_.showMessage(type, `${message} ${title}`);
_.scrollToTop();
}
const showError = () => displayMessage('error', 'Failed to update');
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently watching // change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) { if (isNaN(watchedCount) || watchedCount === 0) {
@ -89,36 +98,31 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
dataType: 'json', dataType: 'json',
type: 'POST', type: 'POST',
success: (res) => { success: (res) => {
const resData = JSON.parse(res); try {
const resData = JSON.parse(res);
if (resData.error) { // Do a rough sanity check for weird errors
_.hide('#loading-shadow'); let updatedProgress = getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.progress');
_.showMessage('error', `Failed to update ${title}. `); if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
_.scrollToTop(); showError();
return;
}
return; // We've completed the series
if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
_.hide(parentSel);
displayMessage('success', 'Completed')
return;
}
// Just a normal update
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
displayMessage('success', 'Updated');
} catch (_) {
showError();
} }
// We've completed the series
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
_.hide(parentSel);
_.hide('#loading-shadow');
_.showMessage('success', `Successfully completed ${title}`);
_.scrollToTop();
return;
}
_.hide('#loading-shadow');
_.showMessage('success', `Successfully updated ${title}`);
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
_.scrollToTop();
}, },
error: () => { error: showError,
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
}
}); });
}); });

103
frontEndSrc/js/fns.js Normal file
View File

@ -0,0 +1,103 @@
/**
* Make sure properties are in an easily splittable format
*
* @private
* @param {String} props
* @param {String} [sep='.'] The default separator
* @return {String}
*/
function _normalizeProperty(props, sep = '.') {
// Since we split by period, and property lookup
// is the same by dot or [], replace bracket lookups
// with periods
return props.replace(/\[(.*?)]/g, sep + '$1');
}
/**
* Tell if a nested object has a given property (or array a given index)
* given an object such as a.b.c.d = 5, hasNestedProperty(a, 'b.c.d') will return true.
*
* @param {Object} object the object to get the property from
* @param {String} property the path to the property as a string
* @returns {boolean} true when property in object, false otherwise
*/
export function hasNestedProperty(object, property) {
if (object && typeof object === 'object') {
if (typeof property === 'string' && property !== '') {
property = _normalizeProperty(property);
let split = property.split('.');
return split.reduce((obj, prop, idx, array) => {
if (idx === array.length - 1) {
return !!(obj && obj.hasOwnProperty(prop));
}
return obj && obj[prop];
}, object);
} else if (typeof property === 'number') {
return property in object;
}
}
return false;
}
/**
* Get the value of a deeply nested property in an object
*
* @param {Object} object the object to get the property
* @param {string} property the path to the property as a string
* @param {string} [sep='.'] The default separator to split on
* @return {*} the value of the property
*/
export function getNestedProperty(object, property, sep = '.') {
if (isType('string', property) && property !== '') {
// convert numbers to dot syntax
property = _normalizeProperty(property, sep);
const levels = property.split(sep);
try {
return levels.reduce((obj, prop) => obj[prop], object);
} catch (e) {
return undefined;
}
}
return null;
}
/**
* Reliably get the type of the value of a variable
*
* @param {*} x The variable to get the type of
* @return {string} The name of the type
*/
export function getType(x) {
// is it an array?
if (Array.isArray(x)) {
return 'array';
}
// Use typeof for truthy primitives
if (typeof x !== 'object') {
return (typeof x).toLowerCase();
}
const type = function () {
return Object.prototype.toString.call(this).slice(8, -1);
}
// Otherwise, strip the type out of the '[Object x]' toString value
return type.call(x).toLowerCase();
}
/**
* Check whether the value matches the passed type name
*
* @param {string} type Javascript type name
* @param {*} val The value to type check
* @return {boolean}
*/
export function isType(type, val) {
return getType(val) === String(type).toLowerCase();
}

View File

@ -1,5 +1,6 @@
import _ from './anime-client.js' import _ from './anime-client.js'
import { renderSearchResults } from './template-helpers.js' import { renderSearchResults } from './template-helpers.js'
import { getNestedProperty, hasNestedProperty } from "./fns";
const search = (query) => { const search = (query) => {
_.show('.cssload-loader'); _.show('.cssload-loader');
@ -36,7 +37,7 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume'; let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume';
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0; let completed = parseInt(_.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0;
let total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10); let total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10);
let mangaName = _.$('.name', parentSel)[ 0 ].textContent; let title = _.$('.name', parentSel)[ 0 ].textContent;
if (isNaN(completed)) { if (isNaN(completed)) {
completed = 0; completed = 0;
@ -45,12 +46,21 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
// Setup the update data // Setup the update data
let data = { let data = {
id: parentSel.dataset.kitsuId, id: parentSel.dataset.kitsuId,
anilist_id: parentSel.dataset.anilistId,
mal_id: parentSel.dataset.malId, mal_id: parentSel.dataset.malId,
data: { data: {
progress: completed progress: completed
} }
}; };
const displayMessage = (type, message) => {
_.hide('#loading-shadow');
_.showMessage(type, `${message} ${title}`);
_.scrollToTop();
}
const showError = () => displayMessage('error', 'Failed to update');
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently reading // change status to currently reading
if (isNaN(completed) || completed === 0) { if (isNaN(completed) || completed === 0) {
@ -73,33 +83,32 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
type: 'POST', type: 'POST',
mimeType: 'application/json', mimeType: 'application/json',
success: (res) => { success: (res) => {
const resData = JSON.parse(res) try {
if (resData.error) { const resData = JSON.parse(res);
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${mangaName}. `); // Do a rough sanity check for weird errors
_.scrollToTop(); let updatedProgress = getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.progress');
return; if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
showError();
return;
}
// We've completed the series
if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
_.hide(parentSel);
displayMessage('success', 'Completed')
return;
}
// Just a normal update
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
displayMessage('success', 'Updated');
} catch (_) {
showError();
} }
if (String(data.data.status).toUpperCase() === 'COMPLETED') {
_.hide(parentSel);
_.hide('#loading-shadow');
_.showMessage('success', `Successfully completed ${mangaName}`);
_.scrollToTop();
return;
}
_.hide('#loading-shadow');
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
_.showMessage('success', `Successfully updated ${mangaName}`);
_.scrollToTop();
}, },
error: () => { error: showError,
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${mangaName}`);
_.scrollToTop();
}
}); });
}); });

View File

@ -15,7 +15,7 @@
"cssnano": "^5.0.1", "cssnano": "^5.0.1",
"postcss": "^8.2.6", "postcss": "^8.2.6",
"postcss-import": "^15.0.0", "postcss-import": "^15.0.0",
"postcss-preset-env": "^7.8.2", "postcss-preset-env": "^8.0.1",
"watch": "^1.0.2" "watch": "^1.0.2"
} }
} }

View File

@ -1,4 +1,6 @@
module.exports = { const { config } = require("@swc/core/spack");
module.exports = config({
entry: { entry: {
'scripts.min': __dirname + '/js/index.js', 'scripts.min': __dirname + '/js/index.js',
'tables.min': __dirname + '/js/base/sort-tables.js', 'tables.min': __dirname + '/js/base/sort-tables.js',
@ -8,12 +10,15 @@ module.exports = {
}, },
options: { options: {
jsc: { jsc: {
target: 'es3', parser: {
loose: true, syntax: "ecmascript",
jsx: false,
},
target: 'es2016',
loose: false,
}, },
minify: true, minify: true,
module: { sourceMaps: false,
type: 'es6' isModule: true,
}
} }
} });

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,25 @@
default: default:
@just --list @just --list
# Runs rector, showing what changes will be make # -------------------------------------------------------------------
rector-dry-run: # Front-end stuff
tools/vendor/bin/rector process --config=tools/rector.php --dry-run src # -------------------------------------------------------------------
# Runs rector, and updates the files # Builds/optimizes JS and CSS
rector: build:
tools/vendor/bin/rector process --config=tools/rector.php src cd frontEndSrc && npm run build && cd ..
# Builds/optimizes CSS
css:
composer run-script build:css
# Builds/optimizes JS
js:
composer run-script build:js
# -------------------------------------------------------------------
# Code Quality and Formatting
# -------------------------------------------------------------------
# Check code formatting # Check code formatting
check-fmt: check-fmt:
@ -18,6 +30,22 @@ check-fmt:
fmt: fmt:
tools/vendor/bin/php-cs-fixer fix --verbose tools/vendor/bin/php-cs-fixer fix --verbose
# Runs phpstan code check
phpstan:
composer run-script phpstan
# Runs rector, showing what changes will be make
rector-dry-run:
tools/vendor/bin/rector process --config=tools/rector.php --dry-run src tests
# Runs rector, and updates the source files
rector:
tools/vendor/bin/rector process --config=tools/rector.php src tests
# -------------------------------------------------------------------
# Testing
# -------------------------------------------------------------------
# Run tests # Run tests
test: test:
composer run-script test composer run-script test
@ -26,10 +54,14 @@ test:
test-update: test-update:
composer run-script test-update composer run-script test-update
# Update the per-file header comments
update-headers:
php tools/update_header_comments.php
# Run unit tests and generate test-coverage report # Run unit tests and generate test-coverage report
coverage: coverage:
composer run-script coverage composer run-script coverage
# -------------------------------------------------------------------
# Misc
# -------------------------------------------------------------------
# Update the per-file header comments
update-headers:
php tools/update_header_comments.php

View File

@ -9,11 +9,12 @@ parameters:
- ./console - ./console
- index.php - index.php
ignoreErrors: ignoreErrors:
- "#Offset 'fields' does not exist on array#" - '#Unable to resolve the template type T#'
- '#Function imagepalletetotruecolor not found#' - '#imagepalletetotruecolor not found#'
- '#Call to an undefined method Aura\\\Html\\\HelperLocator::[a-zA-Z0-9_]+\(\)#' - '#Call to an undefined method Aura\\\Html\\\HelperLocator::[a-zA-Z0-9_]+\(\)#'
- '#Call to an undefined method Query\\QueryBuilderInterface::[a-zA-Z0-9_]+\(\)#' - '#Call to an undefined method Query\\QueryBuilderInterface::[a-zA-Z0-9_]+\(\)#'
excludes_analyse: excludePaths:
- src/Ion/Type/Stringy.php
- tests/mocks.php - tests/mocks.php
- vendor - vendor
# These are objects that basically can return anything # These are objects that basically can return anything

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
var LightTableSorter=function(){var th=null;var cellIndex=null;var order="";var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase()};var sort=function(a,b){var textA=text(a);var textB=text(b);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){var arrayA=textA.replace("episodes: ","").replace("-",0).split("/");var arrayB=textB.replace("episodes: ","").replace("-",0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};var toggle=function(){var c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=c};var reset=function(){th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName("tbody")[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(function(row){tbody.appendChild(row)})}}};return{init:function(){var ths=document.getElementsByTagName("th");var results=[];for(var i=0,len=ths.length;i<len;i++){var th=ths[i];th.classList.add("sorting");th.classList.add("testing");results.push(th.onclick=onClickEvent)}return results}}}();LightTableSorter.init(); const LightTableSorter=(()=>{let th=null;let cellIndex=null;let order="";const text=row=>row.cells.item(cellIndex).textContent.toLowerCase();const sort=(a,b)=>{let textA=text(a);let textB=text(b);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){let arrayA=textA.replace("episodes: ","").replace("-",0).split("/");let arrayB=textB.replace("episodes: ","").replace("-",0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};const toggle=()=>{const c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=c};const reset=()=>{th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};const onClickEvent=e=>{if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;const tbody=th.offsetParent.getElementsByTagName("tbody")[0];let rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(row=>{tbody.appendChild(row)})}}};return{init:()=>{let ths=document.getElementsByTagName("th");let results=[];for(let i=0,len=ths.length;i<len;i++){let th=ths[i];th.classList.add("sorting");th.classList.add("testing");results.push(th.onclick=onClickEvent)}return results}}})();LightTableSorter.init();

File diff suppressed because one or more lines are too long

View File

@ -1,15 +0,0 @@
{
"name": "Anilist Schema",
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"Anilist": {
"url": "https://graphql.anilist.co",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": true
}
}
}
}

View File

@ -189,7 +189,7 @@ final class Model
*/ */
public function deleteItem(FormItem $data, string $type): ?Request public function deleteItem(FormItem $data, string $type): ?Request
{ {
$mediaId = $this->getMediaId((array)$data, $type); $mediaId = $this->getMediaId((array) $data, $type);
if ($mediaId === NULL) if ($mediaId === NULL)
{ {
return NULL; return NULL;
@ -209,7 +209,7 @@ final class Model
*/ */
public function getListIdFromData(FormItem $data, string $type = 'ANIME'): ?string public function getListIdFromData(FormItem $data, string $type = 'ANIME'): ?string
{ {
$mediaId = $this->getMediaId((array)$data, $type); $mediaId = $this->getMediaId((array) $data, $type);
if ($mediaId === NULL) if ($mediaId === NULL)
{ {
return NULL; return NULL;
@ -244,7 +244,7 @@ final class Model
/** /**
* Find the id to update by * Find the id to update by
*/ */
private function getMediaId (array $data, string $type = 'ANIME'): ?string private function getMediaId(array $data, string $type = 'ANIME'): ?string
{ {
if (isset($data['anilist_id'])) if (isset($data['anilist_id']))
{ {

View File

@ -0,0 +1,8 @@
schema: schema.graphql
extensions:
endpoints:
Anilist:
url: https://graphql.anilist.co
headers:
user-agent: JS GraphQL
introspect: true

View File

@ -1,15 +0,0 @@
{
"name": "Kitsu Schema",
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"Kitsu": {
"url": "https://kitsu.io/api/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": true
}
}
}
}

View File

@ -52,7 +52,7 @@ final class Auth
->getSegment(SESSION_SEGMENT); ->getSegment(SESSION_SEGMENT);
$this->model = $container->get('kitsu-model'); $this->model = $container->get('kitsu-model');
Event::on('::unauthorized::', [$this, 'reAuthenticate']); Event::on('::unauthorized::', $this->reAuthenticate(...));
} }
/** /**
@ -133,7 +133,7 @@ final class Auth
/** /**
* Save the new authentication information * Save the new authentication information
*/ */
private function storeAuth(array|FALSE $auth): bool private function storeAuth(array|false $auth): bool
{ {
if (FALSE !== $auth) if (FALSE !== $auth)
{ {

View File

@ -34,6 +34,7 @@ use Aviat\AnimeClient\API\{
use Aviat\AnimeClient\Enum\MediaType; use Aviat\AnimeClient\Enum\MediaType;
use Aviat\AnimeClient\Kitsu as K; use Aviat\AnimeClient\Kitsu as K;
use Aviat\AnimeClient\Types\{Anime, MangaPage}; use Aviat\AnimeClient\Types\{Anime, MangaPage};
use Aviat\AnimeClient\Types\{AnimeListItem, MangaListItem};
use Aviat\Ion\{ use Aviat\Ion\{
Di\ContainerAware, Di\ContainerAware,
Json Json
@ -282,7 +283,7 @@ final class Model
if ($list === NULL) if ($list === NULL)
{ {
$data = $this->getList(MediaType::ANIME, $status) ?? []; $data = $this->getList(MediaType::ANIME, $status);
// Bail out on no data // Bail out on no data
if (empty($data)) if (empty($data))
@ -319,7 +320,7 @@ final class Model
/** /**
* Get all the anime entries, that are organized for output to html * Get all the anime entries, that are organized for output to html
* *
* @return array<string, mixed[]> * @return array<string, array>
*/ */
public function getFullOrganizedAnimeList(): array public function getFullOrganizedAnimeList(): array
{ {
@ -330,7 +331,7 @@ final class Model
foreach ($statuses as $status) foreach ($statuses as $status)
{ {
$mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status]; $mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
$output[$mappedStatus] = $this->getAnimeList($status) ?? []; $output[$mappedStatus] = $this->getAnimeList($status);
} }
return $output; return $output;
@ -412,7 +413,7 @@ final class Model
if ($list === NULL) if ($list === NULL)
{ {
$data = $this->getList(MediaType::MANGA, $status) ?? []; $data = $this->getList(MediaType::MANGA, $status);
// Bail out on no data // Bail out on no data
if (empty($data)) if (empty($data))
@ -534,14 +535,14 @@ final class Model
* Get the data for a specific list item, generally for editing * Get the data for a specific list item, generally for editing
* *
* @param string $listId - The unique identifier of that list item * @param string $listId - The unique identifier of that list item
* @return mixed
*/ */
public function getListItem(string $listId) public function getListItem(string $listId): AnimeListItem|MangaListItem|array
{ {
$baseData = $this->listItem->get($listId); $baseData = $this->listItem->get($listId);
if ( ! isset($baseData['data']['findLibraryEntryById'])) if ( ! isset($baseData['data']['findLibraryEntryById']))
{ {
return []; // We need to get the errors...
return $baseData;
} }
return (new LibraryEntryTransformer())->transform($baseData['data']['findLibraryEntryById']); return (new LibraryEntryTransformer())->transform($baseData['data']['findLibraryEntryById']);
@ -566,7 +567,7 @@ final class Model
// this way is much faster... // this way is much faster...
foreach ($statuses as $status) foreach ($statuses as $status)
{ {
foreach ($this->getPages([$this, 'getThumbListPages'], strtoupper($type), $status) as $page) foreach ($this->getPages($this->getThumbListPages(...), strtoupper($type), $status) as $page)
{ {
$pages[] = $page; $pages[] = $page;
} }
@ -596,7 +597,7 @@ final class Model
// this way is much faster... // this way is much faster...
foreach ($statuses as $status) foreach ($statuses as $status)
{ {
foreach ($this->getPages([$this, 'getSyncPages'], strtoupper($type), $status) as $page) foreach ($this->getPages($this->getSyncPages(...), strtoupper($type), $status) as $page)
{ {
$pages[] = $page; $pages[] = $page;
} }
@ -626,7 +627,7 @@ final class Model
{ {
$pages = []; $pages = [];
foreach ($this->getPages([$this, 'getListPages'], strtoupper($type), strtoupper($status)) as $page) foreach ($this->getPages($this->getListPages(...), strtoupper($type), strtoupper($status)) as $page)
{ {
$pages[] = $page; $pages[] = $page;
} }
@ -786,7 +787,7 @@ final class Model
} }
} }
private function getUserId(): string protected function getUserId(): string
{ {
static $userId = NULL; static $userId = NULL;

View File

@ -67,9 +67,6 @@ trait MutationTrait
/** /**
* Remove a list item * Remove a list item
*
* @param FormItem $data
* @return Request
*/ */
public function deleteItem(FormItem $data): Request public function deleteItem(FormItem $data): Request
{ {

View File

@ -19,7 +19,7 @@ query ($slug: String!) {
} }
categories(first: 100) { categories(first: 100) {
nodes { nodes {
title title(locales: "en")
} }
} }
characters(first: 100) { characters(first: 100) {
@ -29,7 +29,7 @@ query ($slug: String!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
image { image {
original { original {
@ -50,7 +50,7 @@ query ($slug: String!) {
startCursor startCursor
} }
} }
description description(locales: "en")
startDate startDate
endDate endDate
episodeCount episodeCount
@ -87,7 +87,7 @@ query ($slug: String!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
slug slug
} }
@ -118,7 +118,7 @@ query ($slug: String!) {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
totalLength totalLength
youtubeTrailerVideoId youtubeTrailerVideoId

View File

@ -19,7 +19,7 @@ query ($id: ID!) {
} }
categories(first: 100) { categories(first: 100) {
nodes { nodes {
title title(locales: "en")
} }
} }
characters(first: 100) { characters(first: 100) {
@ -29,7 +29,7 @@ query ($id: ID!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
image { image {
original { original {
@ -50,7 +50,7 @@ query ($id: ID!) {
startCursor startCursor
} }
} }
description description(locales: "en")
startDate startDate
endDate endDate
episodeCount episodeCount
@ -87,7 +87,7 @@ query ($id: ID!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
slug slug
} }
@ -118,7 +118,7 @@ query ($id: ID!) {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
totalLength totalLength
youtubeTrailerVideoId youtubeTrailerVideoId

View File

@ -6,12 +6,12 @@ query ($slug: String!) {
url url
} }
} }
description description(locales: "en")
names { names {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: "*")
}, },
media(first: 100) { media(first: 100) {
nodes { nodes {
@ -22,7 +22,7 @@ query ($slug: String!) {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
posterImage { posterImage {
original { original {
@ -41,7 +41,7 @@ query ($slug: String!) {
type type
} }
role role
voices(first: 10) { voices(first: 10, locale:"*", sort:{direction:ASCENDING, on: UPDATED_AT}) {
nodes { nodes {
id id
locale locale
@ -53,7 +53,7 @@ query ($slug: String!) {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: "*")
} }
image { image {
original { original {
@ -70,4 +70,4 @@ query ($slug: String!) {
} }
slug slug
} }
} }

View File

@ -57,7 +57,7 @@ query (
type type
titles { titles {
canonical canonical
localized localized(locales: "*")
alternatives alternatives
} }
...on Anime { ...on Anime {

View File

@ -16,7 +16,7 @@ query($id: ID!) {
ageRating ageRating
categories(first: 100) { categories(first: 100) {
nodes { nodes {
title title(locales: "*")
} }
} }
mappings(first: 10) { mappings(first: 10) {
@ -41,7 +41,7 @@ query($id: ID!) {
endDate endDate
titles { titles {
canonical canonical
localized localized(locales: "*")
canonicalLocale canonicalLocale
} }
type type

View File

@ -33,7 +33,7 @@ query ($slug: String!) {
titles { titles {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
...on Anime { ...on Anime {
episodeCount episodeCount

View File

@ -19,7 +19,7 @@ query ($slug: String!) {
} }
categories(first: 100) { categories(first: 100) {
nodes { nodes {
title title(locales: "en")
} }
} }
chapterCount chapterCount
@ -51,7 +51,7 @@ query ($slug: String!) {
startCursor startCursor
} }
} }
description description(locales: "en")
startDate startDate
endDate endDate
mappings(first: 10) { mappings(first: 10) {
@ -98,7 +98,7 @@ query ($slug: String!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
slug slug
} }
@ -116,7 +116,7 @@ query ($slug: String!) {
titles { titles {
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
} }
} }

View File

@ -19,7 +19,7 @@ query ($id: ID!) {
} }
categories(first: 100) { categories(first: 100) {
nodes { nodes {
title title(locales: "*")
} }
} }
chapterCount chapterCount
@ -116,7 +116,7 @@ query ($id: ID!) {
titles { titles {
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: "*")
} }
} }
} }

View File

@ -1,7 +1,7 @@
query ($slug: String!) { query ($slug: String!) {
findPersonBySlug(slug: $slug) { findPersonBySlug(slug: $slug) {
id id
description description(locales: "en")
birthday birthday
image { image {
original { original {
@ -20,7 +20,7 @@ query ($slug: String!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
mediaStaff(first: 100) { mediaStaff(first: 100) {
nodes { nodes {
@ -47,7 +47,7 @@ query ($slug: String!) {
titles { titles {
alternatives alternatives
canonical canonical
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
} }
} }
@ -91,7 +91,7 @@ query ($slug: String!) {
} }
titles { titles {
canonical canonical
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
} }
} }

View File

@ -26,7 +26,7 @@ query ($type: MediaTypeEnum!) {
} }
categories(first: 100) { categories(first: 100) {
nodes { nodes {
title title(locales: "*")
} }
} }
characters(first: 100) { characters(first: 100) {
@ -36,7 +36,7 @@ query ($type: MediaTypeEnum!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
image { image {
original { original {
@ -90,7 +90,7 @@ query ($type: MediaTypeEnum!) {
names { names {
alternatives alternatives
canonical canonical
localized localized(locales: "*")
} }
slug slug
} }
@ -108,7 +108,7 @@ query ($type: MediaTypeEnum!) {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: "*")
} }
...on Anime { ...on Anime {
episodeCount episodeCount

View File

@ -19,7 +19,7 @@ query ($query: String!) {
slug slug
titles { titles {
canonical canonical
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
alternatives alternatives
} }
myLibraryEntry { myLibraryEntry {

View File

@ -19,7 +19,7 @@ query ($query: String!) {
slug slug
titles { titles {
canonical canonical
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
alternatives alternatives
} }
myLibraryEntry { myLibraryEntry {

View File

@ -18,8 +18,10 @@ query ($slug: String!) {
} }
} }
birthday birthday
createdAt
id id
location location
gender
name name
proMessage proMessage
proTier proTier
@ -52,7 +54,7 @@ query ($slug: String!) {
} }
titles { titles {
canonical canonical
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
} }
...on Manga { ...on Manga {
@ -72,7 +74,7 @@ query ($slug: String!) {
} }
titles { titles {
canonical canonical
localized localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
} }
} }
...on Person { ...on Person {
@ -88,11 +90,12 @@ query ($slug: String!) {
width width
} }
} }
name,
names { names {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: "*")
}, },
} }
...on Character { ...on Character {
@ -107,12 +110,12 @@ query ($slug: String!) {
height height
width width
} }
} },
names { names {
alternatives alternatives
canonical canonical
canonicalLocale canonicalLocale
localized localized(locales: "*")
}, },
} }
} }
@ -150,7 +153,7 @@ query ($slug: String!) {
names { names {
canonical canonical
alternatives alternatives
localized localized(locales: "*")
} }
} }
waifuOrHusbando waifuOrHusbando

View File

@ -78,7 +78,7 @@ final class RequestBuilder extends APIRequestBuilder
elseif ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) elseif ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
{ {
$token = $sessionSegment->get('auth_token'); $token = $sessionSegment->get('auth_token');
if ( ! (empty($token) || $cache->has(K::AUTH_TOKEN_CACHE_KEY))) if ( ! empty($token))
{ {
$cache->set(K::AUTH_TOKEN_CACHE_KEY, $token); $cache->set(K::AUTH_TOKEN_CACHE_KEY, $token);
} }
@ -239,43 +239,4 @@ final class RequestBuilder extends APIRequestBuilder
'body' => $body, 'body' => $body,
]); ]);
} }
/**
* Make a request
*/
private function request(string $type, string $url, array $options = []): array
{
$logger = $this->container->getLogger('kitsu-request');
$response = $this->getResponse($type, $url, $options);
$statusCode = $response->getStatus();
// Check for requests that are unauthorized
if ($statusCode === 401 || $statusCode === 403)
{
Event::emit(EventType::UNAUTHORIZED);
}
$rawBody = wait($response->getBody()->buffer());
// Any other type of failed request
if ($statusCode > 299 || $statusCode < 200)
{
if ($logger !== NULL)
{
$logger->warning('Non 2xx response for api call', (array) $response);
}
}
try
{
return Json::decode($rawBody);
}
catch (JsonException)
{
// dump($e);
dump($rawBody);
exit();
}
}
} }

View File

@ -23,8 +23,6 @@ trait RequestBuilderTrait
/** /**
* Set the request builder object * Set the request builder object
*
* @return ListItem|Model|RequestBuilderTrait
*/ */
public function setRequestBuilder(RequestBuilder $requestBuilder): self public function setRequestBuilder(RequestBuilder $requestBuilder): self
{ {

View File

@ -36,7 +36,8 @@ final class AnimeTransformer extends AbstractTransformer
$characters = []; $characters = [];
$links = []; $links = [];
$staff = []; $staff = [];
$genres = array_map(static fn ($genre) => $genre['title']['en'], $base['categories']['nodes']); $rawGenres = array_filter($base['categories']['nodes'], static fn ($c) => $c !== NULL);
$genres = array_map(static fn ($genre) => $genre['title']['en'], $rawGenres);
sort($genres); sort($genres);
@ -56,7 +57,7 @@ final class AnimeTransformer extends AbstractTransformer
$details = $rawCharacter['character']; $details = $rawCharacter['character'];
$characters[$type][$details['id']] = [ $characters[$type][$details['id']] = [
'image' => $details['image']['original']['url'] ?? '', 'image' => Kitsu::getImage($details),
'name' => $details['names']['canonical'], 'name' => $details['names']['canonical'],
'slug' => $details['slug'], 'slug' => $details['slug'],
]; ];
@ -100,7 +101,7 @@ final class AnimeTransformer extends AbstractTransformer
$staff[$role][$person['id']] = [ $staff[$role][$person['id']] = [
'id' => $person['id'], 'id' => $person['id'],
'name' => $name, 'name' => $name,
'image' => $person['image']['original']['url'], 'image' => Kitsu::getImage($person),
'slug' => $person['slug'], 'slug' => $person['slug'],
]; ];

View File

@ -49,7 +49,7 @@ final class CharacterTransformer extends AbstractTransformer
'castings' => $castings, 'castings' => $castings,
'description' => $data['description']['en'], 'description' => $data['description']['en'],
'id' => $data['id'], 'id' => $data['id'],
'image' => $data['image']['original']['url'] ?? 'images/placeholder.png', 'image' => Kitsu::getImage($data),
'media' => $media, 'media' => $media,
'name' => $name, 'name' => $name,
'names' => $names, 'names' => $names,
@ -130,7 +130,7 @@ final class CharacterTransformer extends AbstractTransformer
'person' => [ 'person' => [
'id' => $voice['person']['id'], 'id' => $voice['person']['id'],
'slug' => $voice['person']['slug'], 'slug' => $voice['person']['slug'],
'image' => $voice['person']['image']['original']['url'], 'image' => Kitsu::getImage($voice['person']),
'name' => $voice['person']['name'], 'name' => $voice['person']['name'],
], ],
'series' => [], 'series' => [],

View File

@ -54,10 +54,10 @@ final class MangaTransformer extends AbstractTransformer
} }
$details = $rawCharacter['character']; $details = $rawCharacter['character'];
if (array_key_exists($details['id'], $characters[$type])) if (array_key_exists($details['id'], (array)$characters[$type]))
{ {
$characters[$type][$details['id']] = [ $characters[$type][$details['id']] = [
'image' => $details['image']['original']['url'], 'image' => Kitsu::getImage($details),
'name' => $details['names']['canonical'], 'name' => $details['names']['canonical'],
'slug' => $details['slug'], 'slug' => $details['slug'],
]; ];
@ -103,7 +103,7 @@ final class MangaTransformer extends AbstractTransformer
'id' => $person['id'], 'id' => $person['id'],
'slug' => $person['slug'], 'slug' => $person['slug'],
'name' => $name, 'name' => $name,
'image' => $person['image']['original']['url'], 'image' => Kitsu::getImage($person),
]; ];
usort($staff[$role], static fn ($a, $b) => $a['name'] <=> $b['name']); usort($staff[$role], static fn ($a, $b) => $a['name'] <=> $b['name']);

View File

@ -35,7 +35,8 @@ final class PersonTransformer extends AbstractTransformer
return Person::from([ return Person::from([
'id' => $data['id'], 'id' => $data['id'],
'name' => $canonicalName, 'name' => $canonicalName,
'image' => $data['image']['original']['url'], 'birthday' => $data['birthday'],
'image' => Kitsu::getImage($data),
'names' => array_diff($data['names']['localized'], [$canonicalName]), 'names' => array_diff($data['names']['localized'], [$canonicalName]),
'description' => $data['description']['en'] ?? '', 'description' => $data['description']['en'] ?? '',
'characters' => $orgData['characters'], 'characters' => $orgData['characters'],
@ -97,7 +98,12 @@ final class PersonTransformer extends AbstractTransformer
{ {
foreach ($data['voices']['nodes'] as $voicing) foreach ($data['voices']['nodes'] as $voicing)
{ {
$character = $voicing['mediaCharacter']['character']; if ($voicing === NULL)
{
continue;
}
$character = $voicing['mediaCharacter']['character'] ?? [];
$charId = $character['id']; $charId = $character['id'];
$rawMedia = $voicing['mediaCharacter']['media']; $rawMedia = $voicing['mediaCharacter']['media'];
$role = strtolower($voicing['mediaCharacter']['role']); $role = strtolower($voicing['mediaCharacter']['role']);
@ -123,7 +129,7 @@ final class PersonTransformer extends AbstractTransformer
'character' => [ 'character' => [
'id' => $character['id'], 'id' => $character['id'],
'slug' => $character['slug'], 'slug' => $character['slug'],
'image' => $character['image']['original']['url'], 'image' => Kitsu::getImage($character),
'canonicalName' => $character['names']['canonical'], 'canonicalName' => $character['names']['canonical'],
], ],
'media' => [ 'media' => [

View File

@ -14,11 +14,11 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Kitsu;
use Aviat\AnimeClient\Types\User; use Aviat\AnimeClient\Types\User;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
use function Aviat\AnimeClient\{formatDate, friendlyTime, getDateDiff};
/** /**
* Transform user profile data for display * Transform user profile data for display
* *
@ -39,15 +39,22 @@ final class UserTransformer extends AbstractTransformer
] : []; ] : [];
return User::from([ return User::from([
'about' => $base['about'], 'about' => $base['about'] ?? '',
'avatar' => $base['avatarImage']['original']['url'], 'avatar' => $base['avatarImage']['original']['url'] ?? NULL,
'birthday' => $base['birthday'] !== NULL
? formatDate($base['birthday']) . ' (' .
friendlyTime(getDateDiff($base['birthday']), 'year') . ')'
: NULL,
'joinDate' => formatDate($base['createdAt']) . ' (' .
friendlyTime(getDateDiff($base['createdAt']), 'day') . ' ago)',
'gender' => $base['gender'],
'favorites' => $this->organizeFavorites($favorites), 'favorites' => $this->organizeFavorites($favorites),
'location' => $base['location'], 'location' => $base['location'],
'name' => $base['name'], 'name' => $base['name'],
'slug' => $base['slug'], 'slug' => $base['slug'],
'stats' => $this->organizeStats($stats), 'stats' => $this->organizeStats($stats),
'waifu' => $waifu, 'waifu' => $waifu,
'website' => $base['siteLinks']['nodes'][0]['url'], 'website' => $base['siteLinks']['nodes'][0]['url'] ?? NULL,
]); ]);
} }
@ -81,7 +88,7 @@ final class UserTransformer extends AbstractTransformer
if (array_key_exists('animeAmountConsumed', $stats)) if (array_key_exists('animeAmountConsumed', $stats))
{ {
$animeStats = [ $animeStats = [
'Time spent watching anime:' => Kitsu::friendlyTime($stats['animeAmountConsumed']['time']), 'Time spent watching anime:' => friendlyTime($stats['animeAmountConsumed']['time']),
'Anime series watched:' => number_format($stats['animeAmountConsumed']['media']), 'Anime series watched:' => number_format($stats['animeAmountConsumed']['media']),
'Anime episodes watched:' => number_format($stats['animeAmountConsumed']['units']), 'Anime episodes watched:' => number_format($stats['animeAmountConsumed']['units']),
]; ];

View File

@ -0,0 +1,8 @@
schema: schema.graphql
extensions:
endpoints:
Kitsu:
url: https://kitsu.io/api/graphql
headers:
user-agent: JS GraphQL
introspect: true

View File

@ -35,7 +35,7 @@ final class ParallelAPIRequest
/** /**
* Add a request * Add a request
*/ */
public function addRequest(string|Request $request, string|int|NULL $key = NULL): self public function addRequest(string|Request $request, string|int|null $key = NULL): self
{ {
if ($key !== NULL) if ($key !== NULL)
{ {
@ -56,7 +56,7 @@ final class ParallelAPIRequest
*/ */
public function addRequests(array $requests): self public function addRequests(array $requests): self
{ {
array_walk($requests, [$this, 'addRequest']); array_walk($requests, $this->addRequest(...));
return $this; return $this;
} }

View File

@ -17,6 +17,7 @@ namespace Aviat\AnimeClient;
use Amp\Http\Client\{HttpClient, HttpClientBuilder, Request, Response}; use Amp\Http\Client\{HttpClient, HttpClientBuilder, Request, Response};
use Aviat\Ion\{ConfigInterface, ImageBuilder}; use Aviat\Ion\{ConfigInterface, ImageBuilder};
use DateTimeImmutable;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Throwable; use Throwable;
@ -25,13 +26,17 @@ use Yosymfony\Toml\{Toml, TomlBuilder};
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
const SECONDS_IN_MINUTE = 60;
const MINUTES_IN_HOUR = 60;
const MINUTES_IN_DAY = 1440;
const MINUTES_IN_YEAR = 525_600;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
//! TOML Functions //! TOML Functions
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/** /**
* Load configuration options from .toml files * Load configuration options from .toml files
* *
* @codeCoverageIgnore
* @param string $path - Path to load config * @param string $path - Path to load config
*/ */
function loadConfig(string $path): array function loadConfig(string $path): array
@ -72,8 +77,6 @@ function loadConfig(string $path): array
/** /**
* Load config from one specific TOML file * Load config from one specific TOML file
*
* @codeCoverageIgnore
*/ */
function loadTomlFile(string $filename): array function loadTomlFile(string $filename): array
{ {
@ -131,19 +134,6 @@ function tomlToArray(string $toml): array
//! Misc Functions //! Misc Functions
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
if ( ! function_exists('array_is_list'))
{
/**
* Polyfill for PHP 8
*
* @see https://www.php.net/manual/en/function.array-is-list
*/
function array_is_list(array $a): bool
{
return $a === [] || (array_keys($a) === range(0, count($a) - 1));
}
}
/** /**
* Is the array sequential, not associative? * Is the array sequential, not associative?
*/ */
@ -256,8 +246,6 @@ function getLocalImg(string $kitsuUrl, bool $webp = TRUE): string
/** /**
* Create a transparent placeholder image * Create a transparent placeholder image
*
* @codeCoverageIgnore
*/ */
function createPlaceholderImage(string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool function createPlaceholderImage(string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool
{ {
@ -303,15 +291,13 @@ function clearCache(CacheInterface $cache): bool
$cleared = $cache->clear(); $cleared = $cache->clear();
$saved = (empty($userData)) ? TRUE : $cache->setMultiple($userData); $saved = empty($userData) || $cache->setMultiple($userData);
return $cleared && $saved; return $cleared && $saved;
} }
/** /**
* Render a PHP code template as a string * Render a PHP code template as a string
*
* @codeCoverageIgnore
*/ */
function renderTemplate(string $path, array $data): string function renderTemplate(string $path, array $data): string
{ {
@ -322,3 +308,87 @@ function renderTemplate(string $path, array $data): string
return (is_string($rawOutput)) ? $rawOutput : ''; return (is_string($rawOutput)) ? $rawOutput : '';
} }
function formatDate(string $date): string
{
$date = new DateTimeImmutable($date);
return $date->format('F d, Y');
}
function getDateDiff(string $date): int
{
$now = new DateTimeImmutable();
$then = new DateTimeImmutable($date);
$interval = $now->diff($then, TRUE);
$years = $interval->y * SECONDS_IN_MINUTE * MINUTES_IN_YEAR;
$days = $interval->d * SECONDS_IN_MINUTE * MINUTES_IN_DAY;
$hours = $interval->h * SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
$minutes = $interval->i * SECONDS_IN_MINUTE;
$seconds = $interval->s;
return $years + $days + $hours + $minutes + $seconds;
}
/**
* Convert a time in seconds to a more human-readable format
*/
function friendlyTime(int $seconds, string $minUnit = 'second'): string
{
// All the seconds left
$remSeconds = $seconds % SECONDS_IN_MINUTE;
$minutes = ($seconds - $remSeconds) / SECONDS_IN_MINUTE;
// Minutes short of a year
$years = (int) floor($minutes / MINUTES_IN_YEAR);
$minutes %= MINUTES_IN_YEAR;
// Minutes short of a day
$extraMinutes = $minutes % MINUTES_IN_DAY;
$days = ($minutes - $extraMinutes) / MINUTES_IN_DAY;
// Minutes short of an hour
$remMinutes = $extraMinutes % MINUTES_IN_HOUR;
$hours = ($extraMinutes - $remMinutes) / MINUTES_IN_HOUR;
$parts = [];
foreach ([
'year' => $years,
'day' => $days,
'hour' => $hours,
'minute' => $remMinutes,
'second' => $remSeconds,
] as $label => $value)
{
if ($value === 0)
{
continue;
}
if ($value > 1)
{
$label .= 's';
}
$parts[] = "{$value} {$label}";
if ($label === $minUnit || $label === $minUnit . 's')
{
break;
}
}
$last = array_pop($parts);
if (empty($parts))
{
return $last ?? '';
}
return (count($parts) > 1)
? implode(', ', $parts) . ", and {$last}"
: "{$parts[0]}, {$last}";
}

View File

@ -44,7 +44,7 @@ abstract class BaseCommand extends Command
/** /**
* Echo text in a box * Echo text in a box
*/ */
public function echoBox(string|array $message, string|int|NULL $fgColor = NULL, string|int|NULL $bgColor = NULL): void public function echoBox(string|array $message, string|int|null $fgColor = NULL, string|int|null $bgColor = NULL): void
{ {
if (is_array($message)) if (is_array($message))
{ {
@ -131,7 +131,7 @@ abstract class BaseCommand extends Command
return $this->_di($configArray, $APP_DIR); return $this->_di($configArray, $APP_DIR);
} }
private function _line(string $message, int|string|NULL $fgColor = NULL, int|string|NULL $bgColor = NULL): void private function _line(string $message, int|string|null $fgColor = NULL, int|string|null $bgColor = NULL): void
{ {
if ($fgColor !== NULL) if ($fgColor !== NULL)
{ {

View File

@ -98,21 +98,23 @@ final class SyncLists extends BaseCommand
if ( ! $anilistEnabled) if ( ! $anilistEnabled)
{ {
$this->echoErrorBox('Anlist API is not enabled. Can not sync.'); $this->echoErrorBox('Anlist API is not enabled. Can not sync.');
return false;
return FALSE;
} }
// Authentication is required to update Kitsu // Authentication is required to update Kitsu
$isKitsuAuthenticated = $this->container->get('auth')->isAuthenticated(); $isKitsuAuthenticated = $this->container->get('auth')->isAuthenticated();
if ( !$isKitsuAuthenticated) if ( ! $isKitsuAuthenticated)
{ {
$this->echoErrorBox('Kitsu is not authenticated. Kitsu list can not be updated.'); $this->echoErrorBox('Kitsu is not authenticated. Kitsu list can not be updated.');
return false;
return FALSE;
} }
$this->anilistModel = $this->container->get('anilist-model'); $this->anilistModel = $this->container->get('anilist-model');
$this->kitsuModel = $this->container->get('kitsu-model'); $this->kitsuModel = $this->container->get('kitsu-model');
return true; return TRUE;
} }
/** /**
@ -148,7 +150,7 @@ final class SyncLists extends BaseCommand
*/ */
protected function fetch(string $type): array protected function fetch(string $type): array
{ {
$this->echo("Fetching $type List Data"); $this->echo("Fetching {$type} List Data");
$progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE); $progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE);
$anilist = $this->fetchAnilist($type); $anilist = $this->fetchAnilist($type);

View File

@ -42,6 +42,11 @@ class Controller
{ {
use ContainerAware; use ContainerAware;
/**
* The global configuration object
*/
public ConfigInterface $config;
/** /**
* The authentication object * The authentication object
*/ */
@ -52,11 +57,6 @@ class Controller
*/ */
protected CacheInterface $cache; protected CacheInterface $cache;
/**
* The global configuration object
*/
public ConfigInterface $config;
/** /**
* Request object * Request object
*/ */
@ -120,197 +120,194 @@ class Controller
Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->cache->delete($key)); Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->cache->delete($key));
} }
/** /**
* Set the current url in the session as the target of a future redirect * Set the current url in the session as the target of a future redirect
* *
* @codeCoverageIgnore * @throws ContainerException
* @throws ContainerException * @throws NotFoundException
* @throws NotFoundException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
public function setSessionRedirect(?string $url = NULL): void public function setSessionRedirect(?string $url = NULL): void
{ {
$serverParams = $this->request->getServerParams(); $serverParams = $this->request->getServerParams();
if ( ! array_key_exists('HTTP_REFERER', $serverParams)) if ( ! array_key_exists('HTTP_REFERER', $serverParams))
{ {
return; return;
} }
$util = $this->container->get('util'); $util = $this->container->get('util');
$doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri(); $doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri();
$isLoginPage = str_contains($serverParams['HTTP_REFERER'], 'login'); $isLoginPage = str_contains($serverParams['HTTP_REFERER'], 'login');
// Don't attempt to set the redirect url if // Don't attempt to set the redirect url if
// the page is one of the form type pages, // the page is one of the form type pages,
// and the previous page is also a form type // and the previous page is also a form type
if ($doubleFormPage || $isLoginPage) if ($doubleFormPage || $isLoginPage)
{ {
return; return;
} }
if (NULL === $url) if (NULL === $url)
{ {
$url = $util->isViewPage() $url = $util->isViewPage()
? (string) $this->request->getUri() ? (string) $this->request->getUri()
: $serverParams['HTTP_REFERER']; : $serverParams['HTTP_REFERER'];
} }
$this->session->set('redirect_url', $url); $this->session->set('redirect_url', $url);
} }
/** /**
* Redirect to the url previously set in the session * Redirect to the url previously set in the session
* *
* If one is not set, redirect to default url * If one is not set, redirect to default url
* *
* @codeCoverageIgnore * @throws InvalidArgumentException
* @throws InvalidArgumentException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
public function sessionRedirect(): void public function sessionRedirect(): void
{ {
$target = $this->session->get('redirect_url') ?? '/'; $target = $this->session->get('redirect_url') ?? '/';
$this->redirect($target, 303); $this->redirect($target, 303);
$this->session->set('redirect_url', NULL); $this->session->set('redirect_url', NULL);
} }
/** /**
* Check if the current user is authenticated, else error and exit * Check if the current user is authenticated, else error and exit
* @codeCoverageIgnore */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
protected function checkAuth(): void protected function checkAuth(): void
{ {
if ( ! $this->auth->isAuthenticated()) if ( ! $this->auth->isAuthenticated())
{ {
$this->errorPage( $this->errorPage(
403, 403,
'Forbidden', 'Forbidden',
'You must <a href="/login">log in</a> to perform this action.' 'You must <a href="/login">log in</a> to perform this action.'
); );
} }
} }
/** /**
* Get the string output of a partial template * Get the string output of a partial template
* */
* @codeCoverageIgnore #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
*/ protected function loadPartial(HtmlView $view, string $template, array $data = []): string
protected function loadPartial(HtmlView $view, string $template, array $data = []): string {
{ $router = $this->container->get('dispatcher');
$router = $this->container->get('dispatcher');
if (isset($this->baseData)) if (isset($this->baseData))
{ {
$data = array_merge($this->baseData, $data); $data = array_merge($this->baseData, $data);
} }
$route = $router->getRoute(); $route = $router->getRoute();
$data['route_path'] = $route !== FALSE ? $route->path : ''; $data['route_path'] = $route !== FALSE ? $route->path : '';
$templatePath = _dir($this->config->get('view_path'), "{$template}.php"); $templatePath = _dir($this->config->get('view_path'), "{$template}.php");
if ( ! is_file($templatePath)) if ( ! is_file($templatePath))
{ {
throw new InvalidArgumentException("Invalid template : {$template}"); throw new InvalidArgumentException("Invalid template : {$template}");
} }
return $view->renderTemplate($templatePath, $data); return $view->renderTemplate($templatePath, $data);
} }
/** /**
* Render a template with header and footer * Render a template with header and footer
* */
* @codeCoverageIgnore #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
*/ protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView
protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView {
{ $csp = [
$csp = [ "default-src 'self' media.kitsu.io kitsu-production-media.s3.us-west-002.backblazeb2.com",
"default-src 'self' media.kitsu.io kitsu-production-media.s3.us-west-002.backblazeb2.com", "object-src 'none'",
"object-src 'none'", "child-src 'self' *.youtube.com polyfill.io",
"child-src 'self' *.youtube.com polyfill.io", ];
];
$view->addHeader('Content-Security-Policy', implode('; ', $csp)); $view->addHeader('Content-Security-Policy', implode('; ', $csp));
$view->appendOutput($this->loadPartial($view, 'header', $data)); $view->appendOutput($this->loadPartial($view, 'header', $data));
if (array_key_exists('message', $data) && is_array($data['message'])) if (array_key_exists('message', $data) && is_array($data['message']))
{ {
$view->appendOutput($this->loadPartial($view, 'message', $data['message'])); $view->appendOutput($this->loadPartial($view, 'message', $data['message']));
} }
$view->appendOutput($this->loadPartial($view, $template, $data)); $view->appendOutput($this->loadPartial($view, $template, $data));
$view->appendOutput($this->loadPartial($view, 'footer', $data)); $view->appendOutput($this->loadPartial($view, 'footer', $data));
return $view; return $view;
} }
/** /**
* 404 action * 404 action
* *
* @codeCoverageIgnore * @throws InvalidArgumentException
* @throws InvalidArgumentException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
public function notFound( public function notFound(
string $title = 'Sorry, page not found', string $title = 'Sorry, page not found',
string $message = 'Page Not Found' string $message = 'Page Not Found'
): void { ): never {
$this->outputHTML('404', [ $this->outputHTML('404', [
'title' => $title, 'title' => $title,
'message' => $message, 'message' => $message,
], NULL, 404); ], NULL, 404);
exit(); exit();
} }
/** /**
* Display a generic error page * Display a generic error page
* *
* @codeCoverageIgnore * @throws InvalidArgumentException
* @throws InvalidArgumentException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
public function errorPage(int $httpCode, string $title, string $message, string $longMessage = ''): void public function errorPage(int $httpCode, string $title, string $message, string $longMessage = ''): void
{ {
$this->outputHTML('error', [ $this->outputHTML('error', [
'title' => $title, 'title' => $title,
'message' => $message, 'message' => $message,
'long_message' => $longMessage, 'long_message' => $longMessage,
], NULL, $httpCode); ], NULL, $httpCode);
} }
/** /**
* Redirect to the default controller/url from an empty path * Redirect to the default controller/url from an empty path
* *
* @codeCoverageIgnore * @throws InvalidArgumentException
* @throws InvalidArgumentException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
public function redirectToDefaultRoute(): void public function redirectToDefaultRoute(): void
{ {
$defaultType = $this->config->get('default_list'); $defaultType = $this->config->get('default_list');
$this->redirect($this->urlGenerator->defaultUrl($defaultType), 303); $this->redirect($this->urlGenerator->defaultUrl($defaultType), 303);
} }
/** /**
* Set a session flash variable to display a message on * Set a session flash variable to display a message on
* next page load * next page load
* */
* @codeCoverageIgnore #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
*/ public function setFlashMessage(string $message, string $type = 'info'): void
public function setFlashMessage(string $message, string $type = 'info'): void {
{ static $messages;
static $messages;
if ( ! $messages) if ( ! $messages)
{ {
$messages = []; $messages = [];
} }
$messages[] = [ $messages[] = [
'message_type' => $type, 'message_type' => $type,
'message' => $message, 'message' => $message,
]; ];
$this->session->setFlash('message', $messages); $this->session->setFlash('message', $messages);
} }
/** /**
* Helper for consistent page titles * Helper for consistent page titles
@ -322,63 +319,62 @@ class Controller
return implode(' &middot; ', $parts); return implode(' &middot; ', $parts);
} }
/** /**
* Add a message box to the page * Add a message box to the page
* *
* @codeCoverageIgnore * @throws InvalidArgumentException
* @throws InvalidArgumentException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
protected function showMessage(HtmlView $view, string $type, string $message): string protected function showMessage(HtmlView $view, string $type, string $message): string
{ {
return $this->loadPartial($view, 'message', [ return $this->loadPartial($view, 'message', [
'message_type' => $type, 'message_type' => $type,
'message' => $message, 'message' => $message,
]); ]);
} }
/** /**
* Output a template to HTML, using the provided data * Output a template to HTML, using the provided data
* *
* @codeCoverageIgnore * @throws InvalidArgumentException
* @throws InvalidArgumentException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
protected function outputHTML(string $template, array $data = [], ?HtmlView $view = NULL, int $code = 200): void protected function outputHTML(string $template, array $data = [], ?HtmlView $view = NULL, int $code = 200): void
{ {
if (NULL === $view) if (NULL === $view)
{ {
$view = new HtmlView($this->container); $view = new HtmlView($this->container);
} }
$view->setStatusCode($code); $view->setStatusCode($code);
$this->renderFullPage($view, $template, $data)->send(); $this->renderFullPage($view, $template, $data)->send();
} }
/** /**
* Output a JSON Response * Output a JSON Response
* *
* @codeCoverageIgnore * @param int $code - the http status code
* @param int $code - the http status code * @throws DoubleRenderException
* @throws DoubleRenderException */
*/ #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
protected function outputJSON(mixed $data, int $code): void protected function outputJSON(mixed $data, int $code): void
{ {
JsonView::new() JsonView::new()
->setOutput($data) ->setOutput($data)
->setStatusCode($code) ->setStatusCode($code)
->send(); ->send();
} }
/** /**
* Redirect to the selected page * Redirect to the selected page
* */
* @codeCoverageIgnore #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
*/ protected function redirect(string $url, int $code): void
protected function redirect(string $url, int $code): void {
{ HttpView::new()
HttpView::new() ->redirect($url, $code)
->redirect($url, $code) ->send();
->send(); }
}
} }
// End of BaseController.php // End of BaseController.php

View File

@ -21,8 +21,7 @@ use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Anime as AnimeModel; use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Types\FormItem; use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Json; use Aviat\Ion\Json;
@ -307,8 +306,6 @@ final class Anime extends BaseController
'Anime not found', 'Anime not found',
'Anime Not Found' 'Anime Not Found'
); );
return;
} }
$this->outputHTML('anime/details', [ $this->outputHTML('anime/details', [
@ -346,8 +343,6 @@ final class Anime extends BaseController
'Anime not found', 'Anime not found',
'Anime Not Found' 'Anime Not Found'
); );
return;
} }
$this->outputHTML('anime/details', [ $this->outputHTML('anime/details', [

View File

@ -20,11 +20,9 @@ use Aviat\AnimeClient\Model\{
Anime as AnimeModel, Anime as AnimeModel,
AnimeCollection as AnimeCollectionModel AnimeCollection as AnimeCollectionModel
}; };
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Json;
use Aviat\Ion\Exception\DoubleRenderException; use Aviat\Ion\Exception\DoubleRenderException;
use InvalidArgumentException; use InvalidArgumentException;
@ -114,7 +112,6 @@ final class AnimeCollection extends BaseController
/** /**
* Show the anime collection add/edit form * Show the anime collection add/edit form
* *
* @param int|null $id
* @throws ContainerException * @throws ContainerException
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws NotFoundException * @throws NotFoundException

View File

@ -18,8 +18,7 @@ use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
@ -60,8 +59,6 @@ final class Character extends BaseController
), ),
'Character Not Found' 'Character Not Found'
); );
return;
} }
$data = (new CharacterTransformer())->transform($rawData)->toArray(); $data = (new CharacterTransformer())->transform($rawData)->toArray();

View File

@ -14,9 +14,8 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\AnimeClient\{Controller as BaseController, Model}; use Aviat\AnimeClient\{Controller as BaseController, Model};
use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};

View File

@ -15,8 +15,7 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Throwable; use Throwable;
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\{createPlaceholderImage, getResponse}; use function Aviat\AnimeClient\{createPlaceholderImage, getResponse};
@ -97,7 +96,7 @@ final class Images extends BaseController
$kitsuUrl .= $imageType['kitsuUrl']; $kitsuUrl .= $imageType['kitsuUrl'];
$width = $imageType['width']; $width = $imageType['width'];
$height = $imageType['height']; $height = $imageType['height'] ?? 225;
$filePrefix = "{$baseSavePath}/{$type}/{$id}"; $filePrefix = "{$baseSavePath}/{$type}/{$id}";
$response = getResponse($kitsuUrl); $response = getResponse($kitsuUrl);
@ -121,11 +120,11 @@ final class Images extends BaseController
if ($display) if ($display)
{ {
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height); $this->getPlaceholder("{$baseSavePath}/{$type}", $width ?? 225, $height);
} }
else else
{ {
createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height); createPlaceholderImage("{$baseSavePath}/{$type}", $width ?? 225, $height);
} }
return; return;
@ -133,7 +132,13 @@ final class Images extends BaseController
$data = wait($response->getBody()->buffer()); $data = wait($response->getBody()->buffer());
[$origWidth] = getimagesizefromstring($data); $size = getimagesizefromstring($data);
if ($size === FALSE)
{
return;
}
[$origWidth] = $size;
$gdImg = imagecreatefromstring($data); $gdImg = imagecreatefromstring($data);
if ($gdImg === FALSE) if ($gdImg === FALSE)
{ {
@ -183,15 +188,15 @@ final class Images extends BaseController
/** /**
* Get a placeholder for a missing image * Get a placeholder for a missing image
*/ */
private function getPlaceholder(string $path, ?int $width = 200, ?int $height = NULL): void private function getPlaceholder(string $path, ?int $width = NULL, ?int $height = NULL): void
{ {
$height ??= $width; $height ??= $width ?? 200;
$filename = $path . '/placeholder.png'; $filename = $path . '/placeholder.png';
if ( ! file_exists($path . '/placeholder.png')) if ( ! file_exists($path . '/placeholder.png'))
{ {
createPlaceholderImage($path, $width, $height); createPlaceholderImage($path, $width ?? 200, $height);
} }
header('Content-Type: image/png'); header('Content-Type: image/png');

View File

@ -14,21 +14,16 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aura\Router\Exception\RouteNotFound;
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Manga as MangaModel; use Aviat\AnimeClient\Model\Manga as MangaModel;
use Aviat\AnimeClient\Types\FormItem; use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Json; use Aviat\Ion\Json;
use InvalidArgumentException;
use Throwable;
/** /**
* Controller for manga list * Controller for manga list
*/ */
@ -282,8 +277,6 @@ final class Manga extends BaseController
'Manga not found', 'Manga not found',
'Manga Not Found' 'Manga Not Found'
); );
return;
} }
$this->outputHTML('manga/details', [ $this->outputHTML('manga/details', [
@ -311,8 +304,6 @@ final class Manga extends BaseController
'Manga not found', 'Manga not found',
'Manga Not Found' 'Manga Not Found'
); );
return;
} }
$this->outputHTML('manga/details', [ $this->outputHTML('manga/details', [

View File

@ -15,12 +15,10 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Model; use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\{CharacterTransformer, PersonTransformer};
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Enum\EventType; use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Attribute\DefaultController; use Aviat\Ion\Attribute\{DefaultController, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Event; use Aviat\Ion\Event;
use Aviat\Ion\View\HtmlView; use Aviat\Ion\View\HtmlView;
@ -103,11 +101,7 @@ final class Misc extends BaseController
} }
$this->setFlashMessage('Invalid username or password.'); $this->setFlashMessage('Invalid username or password.');
$this->redirect($this->url->generate('login'), 303);
$redirectUrl = $this->url->generate('login');
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
$this->redirect($redirectUrl, 303);
} }
/** /**
@ -147,8 +141,6 @@ final class Misc extends BaseController
), ),
'Character Not Found' 'Character Not Found'
); );
return;
} }
$data = (new CharacterTransformer())->transform($rawData)->toArray(); $data = (new CharacterTransformer())->transform($rawData)->toArray();
@ -180,8 +172,6 @@ final class Misc extends BaseController
), ),
'Person Not Found' 'Person Not Found'
); );
return;
} }
$this->outputHTML('person/details', [ $this->outputHTML('person/details', [

View File

@ -18,8 +18,7 @@ use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
@ -61,8 +60,6 @@ final class People extends BaseController
), ),
'Person Not Found' 'Person Not Found'
); );
return;
} }
$this->outputHTML('person/details', [ $this->outputHTML('person/details', [

View File

@ -18,8 +18,7 @@ use Aura\Router\Exception\RouteNotFound;
use Aviat\AnimeClient\API\Anilist\Model as AnilistModel; use Aviat\AnimeClient\API\Anilist\Model as AnilistModel;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Settings as SettingsModel; use Aviat\AnimeClient\Model\Settings as SettingsModel;
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
@ -87,10 +86,7 @@ final class Settings extends BaseController
? $this->setFlashMessage('Saved config settings.', 'success') ? $this->setFlashMessage('Saved config settings.', 'success')
: $this->setFlashMessage('Failed to save config file.', 'error'); : $this->setFlashMessage('Failed to save config file.', 'error');
$redirectUrl = $this->url->generate('settings'); $this->redirect($this->url->generate('settings'), 303);
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
$this->redirect($redirectUrl, 303);
} }
/** /**
@ -153,9 +149,6 @@ final class Settings extends BaseController
? $this->setFlashMessage('Linked Anilist Account', 'success') ? $this->setFlashMessage('Linked Anilist Account', 'success')
: $this->setFlashMessage('Error Linking Anilist Account', 'error'); : $this->setFlashMessage('Error Linking Anilist Account', 'error');
$redirectUrl = $this->url->generate('settings'); $this->redirect($this->url->generate('settings'), 303);
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
$this->redirect($redirectUrl, 303);
} }
} }

View File

@ -18,8 +18,7 @@ use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller; use Aviat\Ion\Attribute\{Controller, Route};
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
@ -70,6 +69,11 @@ final class User extends BaseController
: $username; : $username;
$rawData = $this->kitsuModel->getUserData($username); $rawData = $this->kitsuModel->getUserData($username);
if ($rawData['data']['findProfileBySlug'] === NULL)
{
$this->notFound('Sorry, user not found', "The user '{$username}' does not seem to exist.");
}
$data = (new UserTransformer())->transform($rawData)->toArray(); $data = (new UserTransformer())->transform($rawData)->toArray();
$this->outputHTML('user/details', [ $this->outputHTML('user/details', [

View File

@ -69,6 +69,44 @@ final class Dispatcher extends RoutingBase
$this->outputRoutes = $this->setupRoutes(); $this->outputRoutes = $this->setupRoutes();
} }
/**
* Handle the current route
*
* @throws ReflectionException
*/
public function __invoke(?object $route = NULL): void
{
$logger = $this->container->getLogger();
if ($route === NULL)
{
$route = $this->getRoute();
$logger?->info('Dispatcher - Route invoke arguments');
$logger?->info(print_r($route, TRUE));
}
if ( ! $route)
{
// If not route was matched, return an appropriate http
// error message
$errorRoute = $this->getErrorParams();
$controllerName = DEFAULT_CONTROLLER;
$actionMethod = $errorRoute['action_method'];
$params = $errorRoute['params'];
$this->call($controllerName, $actionMethod, $params);
return;
}
$parsed = $this->processRoute(new Friend($route));
$controllerName = $parsed['controller_name'];
$actionMethod = $parsed['action_method'];
$params = $parsed['params'];
$this->call($controllerName, $actionMethod, $params);
}
/** /**
* Get the current route object, if one matches * Get the current route object, if one matches
*/ */
@ -100,47 +138,6 @@ final class Dispatcher extends RoutingBase
return $this->outputRoutes; return $this->outputRoutes;
} }
/**
* Handle the current route
*
* @throws ReflectionException
*/
public function __invoke(?object $route = NULL): void
{
$logger = $this->container->getLogger();
if ($route === NULL)
{
$route = $this->getRoute();
if ($logger !== NULL)
{
$logger->info('Dispatcher - Route invoke arguments');
$logger->info(print_r($route, TRUE));
}
}
if ( ! $route)
{
// If not route was matched, return an appropriate http
// error message
$errorRoute = $this->getErrorParams();
$controllerName = DEFAULT_CONTROLLER;
$actionMethod = $errorRoute['action_method'];
$params = $errorRoute['params'];
$this->call($controllerName, $actionMethod, $params);
return;
}
$parsed = $this->processRoute(new Friend($route));
$controllerName = $parsed['controller_name'];
$actionMethod = $parsed['action_method'];
$params = $parsed['params'];
$this->call($controllerName, $actionMethod, $params);
}
/** /**
* Parse out the arguments for the appropriate controller for * Parse out the arguments for the appropriate controller for
* the current route * the current route
@ -183,10 +180,7 @@ final class Dispatcher extends RoutingBase
} }
$logger = $this->container->getLogger(); $logger = $this->container->getLogger();
if ($logger !== NULL) $logger?->info(Json::encode($params));
{
$logger->info(Json::encode($params));
}
return [ return [
'controller_name' => $controllerName, 'controller_name' => $controllerName,
@ -208,10 +202,7 @@ final class Dispatcher extends RoutingBase
$controller = reset($segments); $controller = reset($segments);
$logger = $this->container->getLogger(); $logger = $this->container->getLogger();
if ($logger !== NULL) $logger?->info('Controller: ' . $controller);
{
$logger->info('Controller: ' . $controller);
}
if (empty($controller)) if (empty($controller))
{ {
@ -224,7 +215,7 @@ final class Dispatcher extends RoutingBase
/** /**
* Get the list of controllers in the default namespace * Get the list of controllers in the default namespace
* *
* @return mixed[] * @return array
*/ */
public function getControllerList(): array public function getControllerList(): array
{ {
@ -300,7 +291,6 @@ final class Dispatcher extends RoutingBase
/** /**
* Get the appropriate params for the error page * Get the appropriate params for the error page
* passed on the failed route * passed on the failed route
* @return mixed[][]
*/ */
protected function getErrorParams(): array protected function getErrorParams(): array
{ {
@ -317,14 +307,15 @@ final class Dispatcher extends RoutingBase
$params = []; $params = [];
switch ($failure->failedRule) { switch ($failure?->failedRule)
{
case Rule\Allows::class: case Rule\Allows::class:
$params = [ $params = [
'http_code' => 405, 'http_code' => 405,
'title' => '405 Method Not Allowed', 'title' => '405 Method Not Allowed',
'message' => 'Invalid HTTP Verb', 'message' => 'Invalid HTTP Verb',
]; ];
break; break;
case Rule\Accepts::class: case Rule\Accepts::class:
$params = [ $params = [
@ -332,12 +323,12 @@ final class Dispatcher extends RoutingBase
'title' => '406 Not Acceptable', 'title' => '406 Not Acceptable',
'message' => 'Unacceptable content type', 'message' => 'Unacceptable content type',
]; ];
break; break;
default: default:
// Fall back to a 404 message // Fall back to a 404 message
$actionMethod = NOT_FOUND_METHOD; $actionMethod = NOT_FOUND_METHOD;
break; break;
} }
return [ return [
@ -348,8 +339,6 @@ final class Dispatcher extends RoutingBase
/** /**
* Select controller based on the current url, and apply its relevant routes * Select controller based on the current url, and apply its relevant routes
*
* @return mixed[]
*/ */
protected function setupRoutes(): array protected function setupRoutes(): array
{ {

View File

@ -83,16 +83,16 @@ final class FormGenerator
]; ];
$params['strict'] = TRUE; $params['strict'] = TRUE;
unset($params['attribs']['id']); unset($params['attribs']['id']);
break; break;
case 'string': case 'string':
$params['type'] = 'text'; $params['type'] = 'text';
break; break;
case 'select': case 'select':
$params['type'] = 'select'; $params['type'] = 'select';
$params['options'] = array_flip($form['options']); $params['options'] = array_flip($form['options']);
break; break;
default: default:
break; break;

View File

@ -31,10 +31,6 @@ final class Kitsu
public const ANIME_HISTORY_LIST_CACHE_KEY = 'kitsu-anime-history-list'; public const ANIME_HISTORY_LIST_CACHE_KEY = 'kitsu-anime-history-list';
public const MANGA_HISTORY_LIST_CACHE_KEY = 'kitsu-manga-history-list'; public const MANGA_HISTORY_LIST_CACHE_KEY = 'kitsu-manga-history-list';
public const GRAPHQL_ENDPOINT = 'https://kitsu.io/api/graphql'; public const GRAPHQL_ENDPOINT = 'https://kitsu.io/api/graphql';
public const SECONDS_IN_MINUTE = 60;
public const MINUTES_IN_HOUR = 60;
public const MINUTES_IN_DAY = 1440;
public const MINUTES_IN_YEAR = 525_600;
/** /**
* Determine whether an anime is airing, finished airing, or has not yet aired * Determine whether an anime is airing, finished airing, or has not yet aired
@ -72,18 +68,18 @@ final class Kitsu
} }
$monthMap = [ $monthMap = [
'01' => 'Jan', '01' => 'January',
'02' => 'Feb', '02' => 'February',
'03' => 'Mar', '03' => 'March',
'04' => 'Apr', '04' => 'April',
'05' => 'May', '05' => 'May',
'06' => 'Jun', '06' => 'June',
'07' => 'Jul', '07' => 'July',
'08' => 'Aug', '08' => 'August',
'09' => 'Sep', '09' => 'September',
'10' => 'Oct', '10' => 'October',
'11' => 'Nov', '11' => 'November',
'12' => 'Dec', '12' => 'December',
]; ];
[$startYear, $startMonth, $startDay] = explode('-', $startDate); [$startYear, $startMonth, $startDay] = explode('-', $startDate);
@ -305,7 +301,16 @@ final class Kitsu
{ {
// Really don't care about languages that aren't english // Really don't care about languages that aren't english
// or Japanese for titles // or Japanese for titles
if ( ! in_array($locale, ['en', 'en_us', 'en_jp', 'ja_jp'], TRUE)) if ( ! in_array($locale, [
'en',
'en-jp',
'en-us',
'en_jp',
'en_us',
'ja-jp',
'ja_jp',
'jp',
], TRUE))
{ {
continue; continue;
} }
@ -326,15 +331,29 @@ final class Kitsu
/** /**
* Get the url of the posterImage from Kitsu, with fallbacks * Get the url of the posterImage from Kitsu, with fallbacks
*/ */
public static function getPosterImage(array $base, int $size = 1): string public static function getPosterImage(array $base, int $sizeId = 1): string
{ {
$rawUrl = $base['posterImage']['views'][$size]['url'] $rawUrl = $base['posterImage']['views'][$sizeId]['url']
?? $base['posterImage']['original']['url'] ?? $base['posterImage']['original']['url']
?? '/public/images/placeholder.png'; ?? '/public/images/placeholder.png';
$parts = explode('?', $rawUrl); $parts = explode('?', $rawUrl);
return (empty($parts)) ? $rawUrl : $parts[0]; return $parts[0];
}
/**
* Get the url of the image from Kitsu, with fallbacks
*/
public static function getImage(array $base, int $sizeId = 1): string
{
$rawUrl = $base['image']['original']['url']
?? $base['image']['views'][$sizeId]['url']
?? '/public/images/placeholder.png';
$parts = explode('?', $rawUrl);
return $parts[0];
} }
/** /**
@ -418,62 +437,6 @@ final class Kitsu
]; ];
} }
/**
* Convert a time in seconds to a more human-readable format
*/
public static function friendlyTime(int $seconds): string
{
// All the seconds left
$remSeconds = $seconds % self::SECONDS_IN_MINUTE;
$minutes = ($seconds - $remSeconds) / self::SECONDS_IN_MINUTE;
// Minutes short of a year
$years = (int) floor($minutes / self::MINUTES_IN_YEAR);
$minutes %= self::MINUTES_IN_YEAR;
// Minutes short of a day
$extraMinutes = $minutes % self::MINUTES_IN_DAY;
$days = ($minutes - $extraMinutes) / self::MINUTES_IN_DAY;
// Minutes short of an hour
$remMinutes = $extraMinutes % self::MINUTES_IN_HOUR;
$hours = ($extraMinutes - $remMinutes) / self::MINUTES_IN_HOUR;
$parts = [];
foreach ([
'year' => $years,
'day' => $days,
'hour' => $hours,
'minute' => $remMinutes,
'second' => $remSeconds,
] as $label => $value)
{
if ($value === 0)
{
continue;
}
if ($value > 1)
{
$label .= 's';
}
$parts[] = "{$value} {$label}";
}
$last = array_pop($parts);
if (empty($parts))
{
return $last ?? '';
}
return (count($parts) > 1)
? implode(', ', $parts) . ", and {$last}"
: "{$parts[0]}, {$last}";
}
/** /**
* Determine if an alternate title is unique enough to list * Determine if an alternate title is unique enough to list
*/ */
@ -486,7 +449,7 @@ final class Kitsu
foreach ($existingTitles as $existing) foreach ($existingTitles as $existing)
{ {
$isSubset = mb_substr_count($existing, $title) > 0; $isSubset = mb_substr_count(mb_strtolower($existing), mb_strtolower($title)) > 0;
$diff = levenshtein(mb_strtolower($existing), mb_strtolower($title)); $diff = levenshtein(mb_strtolower($existing), mb_strtolower($title));
if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50) if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50)

View File

@ -36,6 +36,19 @@ final class MenuGenerator extends UrlGenerator
*/ */
protected ServerRequestInterface $request; protected ServerRequestInterface $request;
/**
* MenuGenerator constructor.
*
* @throws ContainerException
* @throws NotFoundException
*/
private function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->helper = $container->get('html-helper');
$this->request = $container->get('request');
}
public static function new(ContainerInterface $container): self public static function new(ContainerInterface $container): self
{ {
return new self($container); return new self($container);
@ -80,19 +93,6 @@ final class MenuGenerator extends UrlGenerator
return (string) $this->helper->ul(); return (string) $this->helper->ul();
} }
/**
* MenuGenerator constructor.
*
* @throws ContainerException
* @throws NotFoundException
*/
private function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->helper = $container->get('html-helper');
$this->request = $container->get('request');
}
/** /**
* Generate the full menu structure from the config files * Generate the full menu structure from the config files
* *

View File

@ -91,7 +91,7 @@ final class AnimeCollection extends Collection
$genres = $this->getGenreList(); $genres = $this->getGenreList();
$media = $this->getMediaList(); $media = $this->getMediaList();
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -133,7 +133,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -349,7 +349,7 @@ final class AnimeCollection extends Collection
->get() ->get()
->fetchAll(PDO::FETCH_ASSOC); ->fetchAll(PDO::FETCH_ASSOC);
if ($mediaRows === FALSE) if (empty($mediaRows))
{ {
return []; return [];
} }
@ -411,7 +411,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -479,7 +479,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -659,7 +659,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -691,7 +691,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -737,7 +737,7 @@ final class AnimeCollection extends Collection
// Add genres associated with each item // Add genres associated with each item
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }

View File

@ -74,7 +74,7 @@ trait MediaTrait
* Get information about a specific list item * Get information about a specific list item
* for editing/updating that item * for editing/updating that item
*/ */
public function getItem(string $itemId): AnimeListItem|MangaListItem public function getItem(string $itemId): AnimeListItem|MangaListItem|array
{ {
return $this->kitsuModel->getListItem($itemId); return $this->kitsuModel->getListItem($itemId);
} }

View File

@ -95,14 +95,7 @@ final class Settings
} }
} }
if (array_key_exists($key, $values) && is_scalar($values[$key])) $value['value'] = array_key_exists($key, $values) && is_scalar($values[$key]) ? $values[$key] : $value['default'] ?? '';
{
$value['value'] = $values[$key];
}
else
{
$value['value'] = $value['default'] ?? '';
}
foreach (['readonly', 'disabled'] as $flag) foreach (['readonly', 'disabled'] as $flag)
{ {

View File

@ -20,37 +20,6 @@ use Stringable;
abstract class AbstractType implements ArrayAccess, Countable, Stringable abstract class AbstractType implements ArrayAccess, Countable, Stringable
{ {
/**
* Populate values for un-serializing data
*/
public static function __set_state(mixed $properties): self
{
return new static($properties);
}
/**
* Check the shape of the object, and return the array equivalent
*/
final public static function check(array $data = []): ?array
{
$currentClass = static::class;
if (get_parent_class($currentClass) !== FALSE)
{
return static::class::from($data)->toArray();
}
return NULL;
}
/**
* Static constructor
*/
final public static function from(mixed $data): static
{
return new static($data);
}
/** /**
* Sets the properties by using the constructor * Sets the properties by using the constructor
*/ */
@ -73,6 +42,14 @@ abstract class AbstractType implements ArrayAccess, Countable, Stringable
} }
} }
/**
* Populate values for un-serializing data
*/
public static function __set_state(mixed $properties): self
{
return new static($properties);
}
/** /**
* See if a property is set * See if a property is set
*/ */
@ -123,6 +100,29 @@ abstract class AbstractType implements ArrayAccess, Countable, Stringable
return print_r($this, TRUE); return print_r($this, TRUE);
} }
/**
* Check the shape of the object, and return the array equivalent
*/
final public static function check(array $data = []): ?array
{
$currentClass = static::class;
if (get_parent_class($currentClass) !== FALSE)
{
return static::class::from($data)->toArray();
}
return NULL;
}
/**
* Static constructor
*/
final public static function from(mixed $data): static
{
return new static($data);
}
/** /**
* Implementing ArrayAccess * Implementing ArrayAccess
*/ */
@ -201,27 +201,25 @@ abstract class AbstractType implements ArrayAccess, Countable, Stringable
return TRUE; return TRUE;
} }
/** #[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
* @codeCoverageIgnore final protected function fromObject(mixed $parent = NULL): float|null|bool|int|array|string
*/ {
final protected function fromObject(mixed $parent = NULL): float|NULL|bool|int|array|string $object = $parent ?? $this;
{
$object = $parent ?? $this;
if (is_scalar($object) || $object === NULL) if (is_scalar($object) || $object === NULL)
{ {
return $object; return $object;
} }
$output = []; $output = [];
foreach ($object as $key => $value) foreach ($object as $key => $value)
{ {
$output[$key] = (is_scalar($value) || empty($value)) $output[$key] = (is_scalar($value) || empty($value))
? $value ? $value
: $this->fromObject((array) $value); : $this->fromObject((array) $value);
} }
return $output; return $output;
} }
} }

View File

@ -21,6 +21,7 @@ final class Person extends AbstractType
{ {
public string $id; public string $id;
public ?string $name; public ?string $name;
public ?string $birthday;
public string $image; public string $image;
public array $names = []; public array $names = [];
public ?string $description; public ?string $description;

View File

@ -21,11 +21,14 @@ final class User extends AbstractType
{ {
public ?string $about; public ?string $about;
public ?string $avatar; public ?string $avatar;
public ?string $birthday;
public string $joinDate;
public ?string $gender;
public ?array $favorites; public ?array $favorites;
public ?string $location; public ?string $location;
public ?string $name; public ?string $name;
public ?string $slug; public ?string $slug;
public ?array $stats; public ?array $stats;
public ?array $waifu; public array $waifu;
public ?string $website; public ?string $website;
} }

View File

@ -17,6 +17,9 @@ namespace Aviat\Ion\Attribute;
use Attribute; use Attribute;
#[Attribute(Attribute::TARGET_CLASS)] #[Attribute(Attribute::TARGET_CLASS)]
class Controller { class Controller
public function __construct(public string $prefix = '') {} {
} public function __construct(public string $prefix = '')
{
}
}

View File

@ -17,4 +17,6 @@ namespace Aviat\Ion\Attribute;
use Attribute; use Attribute;
#[Attribute(Attribute::TARGET_CLASS)] #[Attribute(Attribute::TARGET_CLASS)]
class DefaultController {} class DefaultController
{
}

View File

@ -17,16 +17,15 @@ namespace Aviat\Ion\Attribute;
use Attribute; use Attribute;
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Route { class Route
public const GET = 'get'; {
public const POST = 'post'; final public const GET = 'get';
final public const POST = 'post';
public function __construct( public function __construct(
public string $name, public string $name,
public string $path, public string $path,
public string $verb = self::GET, public string $verb = self::GET,
) ) {
{
} }
} }

View File

@ -39,8 +39,8 @@ class Container implements ContainerInterface
*/ */
public function __construct( public function __construct(
/** /**
* Array of container Generator functions * Array of container Generator functions
*/ */
protected array $container = [] protected array $container = []
) { ) {
$this->loggers = []; $this->loggers = [];

View File

@ -25,7 +25,10 @@ class Json
/** /**
* Encode data in json format * Encode data in json format
* *
* @throws JsonException * @param mixed $data
* @param int $options
* @param int<1, max> $depth
* @return string
*/ */
public static function encode(mixed $data, int $options = 0, int $depth = 512): string public static function encode(mixed $data, int $options = 0, int $depth = 512): string
{ {
@ -54,7 +57,11 @@ class Json
/** /**
* Decode data from json * Decode data from json
* *
* @throws JsonException * @param string|null $json
* @param bool $assoc
* @param int<1, max> $depth
* @param int $options
* @return mixed
*/ */
public static function decode(?string $json, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed public static function decode(?string $json, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed
{ {
@ -74,7 +81,11 @@ class Json
/** /**
* Decode json data loaded from the passed filename * Decode json data loaded from the passed filename
* *
* @throws JsonException * @param string $filename
* @param bool $assoc
* @param int<1, max> $depth
* @param int $options
* @return mixed
*/ */
public static function decodeFile(string $filename, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed public static function decodeFile(string $filename, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed
{ {

View File

@ -33,7 +33,7 @@ abstract class AbstractTransformer implements TransformerInterface
{ {
$list = (array) $collection; $list = (array) $collection;
return array_map([$this, 'transform'], $list); return array_map($this->transform(...), $list);
} }
/** /**

View File

@ -65,14 +65,6 @@ class ArrayType
'pop' => 'array_pop', 'pop' => 'array_pop',
]; ];
/**
* Create an ArrayType wrapper class from an array
*/
public static function from(array $arr): ArrayType
{
return new ArrayType($arr);
}
/** /**
* Create an ArrayType wrapper class * Create an ArrayType wrapper class
*/ */
@ -108,6 +100,14 @@ class ArrayType
throw new InvalidArgumentException("Method '{$method}' does not exist"); throw new InvalidArgumentException("Method '{$method}' does not exist");
} }
/**
* Create an ArrayType wrapper class from an array
*/
public static function from(array $arr): ArrayType
{
return new ArrayType($arr);
}
/** /**
* Does the passed key exist in the current array? * Does the passed key exist in the current array?
*/ */
@ -156,7 +156,7 @@ class ArrayType
/** /**
* Find an array key by its associated value * Find an array key by its associated value
*/ */
public function search(mixed $value, bool $strict = TRUE): int|string|FALSE|null public function search(mixed $value, bool $strict = TRUE): int|string|false|null
{ {
return array_search($value, $this->arr, $strict); return array_search($value, $this->arr, $strict);
} }
@ -172,7 +172,7 @@ class ArrayType
/** /**
* Return the array, or a key * Return the array, or a key
*/ */
public function &get(string|int|NULL $key = NULL): mixed public function &get(string|int|null $key = NULL): mixed
{ {
$value = NULL; $value = NULL;
if ($key === NULL) if ($key === NULL)

View File

@ -24,9 +24,9 @@ final class StringType extends Stringy
/** /**
* Alias for `create` static constructor * Alias for `create` static constructor
*/ */
public static function from(string $str): self public static function from(string $str = '', ?string $encoding = NULL): self
{ {
return self::create($str); return self::create($str, $encoding);
} }
/** /**

File diff suppressed because it is too large Load Diff

View File

@ -34,11 +34,11 @@ class HtmlView extends HttpView
/** /**
* Create the Html View * Create the Html View
*/ */
public function __construct(ContainerInterface $container) public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->setContainer($container); $this->setContainer(func_get_arg(0));
$this->response = new HtmlResponse(''); $this->response = new HtmlResponse('');
} }

View File

@ -20,6 +20,7 @@ use InvalidArgumentException;
use Laminas\Diactoros\Response; use Laminas\Diactoros\Response;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Stringable; use Stringable;
@ -168,10 +169,10 @@ class HttpView implements HttpViewInterface, Stringable
/** /**
* Send the appropriate response * Send the appropriate response
* *
* @codeCoverageIgnore
* @throws DoubleRenderException * @throws DoubleRenderException
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
#[CodeCoverageIgnore]
protected function output(): void protected function output(): void
{ {
if ($this->hasRendered) if ($this->hasRendered)

View File

@ -40,8 +40,10 @@ final class APIRequestBuilderTest extends TestCase
$this->builder->setLogger(new NullLogger()); $this->builder->setLogger(new NullLogger());
} }
public function testGzipRequest(): void public function testGzipRequest(): never
{ {
$this->markTestSkipped('Need new test API');
$request = $this->builder->newRequest('GET', 'gzip') $request = $this->builder->newRequest('GET', 'gzip')
->getFullRequest(); ->getFullRequest();
$response = getResponse($request); $response = getResponse($request);
@ -49,15 +51,19 @@ final class APIRequestBuilderTest extends TestCase
$this->assertTrue($body['gzipped']); $this->assertTrue($body['gzipped']);
} }
public function testInvalidRequestMethod(): void public function testInvalidRequestMethod(): never
{ {
$this->markTestSkipped('Need new test API');
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
$this->builder->newRequest('FOO', 'gzip') $this->builder->newRequest('FOO', 'gzip')
->getFullRequest(); ->getFullRequest();
} }
public function testRequestWithBasicAuth(): void public function testRequestWithBasicAuth(): never
{ {
$this->markTestSkipped('Need new test API');
$request = $this->builder->newRequest('GET', 'headers') $request = $this->builder->newRequest('GET', 'headers')
->setBasicAuth('username', 'password') ->setBasicAuth('username', 'password')
->getFullRequest(); ->getFullRequest();
@ -68,8 +74,10 @@ final class APIRequestBuilderTest extends TestCase
$this->assertSame('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']); $this->assertSame('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
} }
public function testRequestWithQueryString(): void public function testRequestWithQueryString(): never
{ {
$this->markTestSkipped('Need new test API');
$query = [ $query = [
'foo' => 'bar', 'foo' => 'bar',
'bar' => [ 'bar' => [
@ -96,8 +104,10 @@ final class APIRequestBuilderTest extends TestCase
$this->assertSame($expected, $body['args']); $this->assertSame($expected, $body['args']);
} }
public function testFormValueRequest(): void public function testFormValueRequest(): never
{ {
$this->markTestSkipped('Need new test API');
$formValues = [ $formValues = [
'bar' => 'foo', 'bar' => 'foo',
'foo' => 'bar', 'foo' => 'bar',
@ -113,8 +123,10 @@ final class APIRequestBuilderTest extends TestCase
$this->assertSame($formValues, $body['form']); $this->assertSame($formValues, $body['form']);
} }
public function testFullUrlRequest(): void public function testFullUrlRequest(): never
{ {
$this->markTestSkipped('Need new test API');
$data = [ $data = [
'foo' => [ 'foo' => [
'bar' => 1, 'bar' => 1,

View File

@ -38,7 +38,7 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
$this->transformer = new AnimeListTransformer(); $this->transformer = new AnimeListTransformer();
} }
public function testTransform(): void public function testTransform(): never
{ {
$this->markTestSkipped('Old test data'); $this->markTestSkipped('Old test data');
@ -46,11 +46,11 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
$this->assertMatchesSnapshot($actual); $this->assertMatchesSnapshot($actual);
} }
public function dataUntransform(): array public static function dataUntransform(): array
{ {
return [[ return [[
'input' => [ 'input' => [
'id' => 14047981, 'id' => 14_047_981,
'watching_status' => 'current', 'watching_status' => 'current',
'user_rating' => 8, 'user_rating' => 8,
'episodes_watched' => 38, 'episodes_watched' => 38,
@ -60,7 +60,7 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
], ],
], [ ], [
'input' => [ 'input' => [
'id' => 14047981, 'id' => 14_047_981,
'mal_id' => '12345', 'mal_id' => '12345',
'watching_status' => 'current', 'watching_status' => 'current',
'user_rating' => 8, 'user_rating' => 8,
@ -73,7 +73,7 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
], ],
], [ ], [
'input' => [ 'input' => [
'id' => 14047983, 'id' => 14_047_983,
'mal_id' => '12347', 'mal_id' => '12347',
'watching_status' => 'current', 'watching_status' => 'current',
'user_rating' => 0, 'user_rating' => 0,
@ -87,12 +87,10 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
]]; ]];
} }
/** #[\PHPUnit\Framework\Attributes\DataProvider('dataUntransform')]
* @dataProvider dataUntransform public function testUntransform(array $input): void
*/ {
public function testUntransform(array $input): void $actual = $this->transformer->untransform($input);
{ $this->assertMatchesSnapshot($actual);
$actual = $this->transformer->untransform($input); }
$this->assertMatchesSnapshot($actual);
}
} }

View File

@ -37,7 +37,7 @@ final class AnimeTransformerTest extends AnimeClientTestCase
$this->transformer = new AnimeTransformer(); $this->transformer = new AnimeTransformer();
} }
public function testTransform() public function testTransform(): never
{ {
$this->markTestSkipped('May fail on CI'); $this->markTestSkipped('May fail on CI');
$actual = $this->transformer->transform($this->beforeTransform); $actual = $this->transformer->transform($this->beforeTransform);

View File

@ -35,7 +35,7 @@ final class CharacterTransformerTest extends AnimeClientTestCase
$this->beforeTransform = $raw; $this->beforeTransform = $raw;
} }
public function testTransform(): void public function testTransform(): never
{ {
$this->markTestSkipped('Fails on CI'); $this->markTestSkipped('Fails on CI');
$actual = (new CharacterTransformer())->transform($this->beforeTransform); $actual = (new CharacterTransformer())->transform($this->beforeTransform);

View File

@ -35,7 +35,7 @@ final class HistoryTransformerTest extends AnimeClientTestCase
$this->beforeTransform = $raw; $this->beforeTransform = $raw;
} }
public function testAnimeTransform(): void public function testAnimeTransform(): never
{ {
$this->markTestSkipped('Old test data'); $this->markTestSkipped('Old test data');

View File

@ -35,7 +35,7 @@ final class PersonTransformerTest extends AnimeClientTestCase
$this->beforeTransform = $raw; $this->beforeTransform = $raw;
} }
public function testTransform(): void public function testTransform(): never
{ {
$this->markTestSkipped('Fails on CI'); $this->markTestSkipped('Fails on CI');
$actual = (new PersonTransformer())->transform($this->beforeTransform); $actual = (new PersonTransformer())->transform($this->beforeTransform);

View File

@ -38,6 +38,10 @@ final class UserTransformerTest extends AnimeClientTestCase
public function testTransform(): void public function testTransform(): void
{ {
$actual = (new UserTransformer())->transform($this->beforeTransform); $actual = (new UserTransformer())->transform($this->beforeTransform);
// Unset the time value that will change every day, so the test is consistent
$actual->joinDate = '';
$this->assertMatchesSnapshot($actual); $this->assertMatchesSnapshot($actual);
} }
} }

View File

@ -17,7 +17,7 @@ id: '20286'
manga_type: MANGA manga_type: MANGA
status: Completed status: Completed
staff: staff:
'Story & Art': [{ id: '8712', slug: ruri-miyahara, name: 'Ruri Miyahara', image: 'https://media.kitsu.io/people/images/8712/original.jpg?1533271952' }] 'Story & Art': [{ id: '8712', slug: ruri-miyahara, name: 'Ruri Miyahara', image: 'https://media.kitsu.io/people/images/8712/original.jpg' }]
synopsis: "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\r\n(Source: Kirei Cake)" synopsis: "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\r\n(Source: Kirei Cake)"
title: 'Bokura wa Minna Kawai-sou' title: 'Bokura wa Minna Kawai-sou'
titles: titles:

View File

@ -15,11 +15,17 @@
namespace Aviat\AnimeClient\Tests; namespace Aviat\AnimeClient\Tests;
use DateTime; use DateTime;
use function Aviat\AnimeClient\{arrayToToml, checkFolderPermissions, clearCache, colNotEmpty, getLocalImg, getResponse, isSequentialArray, tomlToArray}; use PHPUnit\Framework\Attributes\IgnoreFunctionForCodeCoverage;
use function Aviat\AnimeClient\{arrayToToml, checkFolderPermissions, clearCache, colNotEmpty, friendlyTime, getLocalImg, getResponse, isSequentialArray, tomlToArray};
use const Aviat\AnimeClient\{MINUTES_IN_DAY, MINUTES_IN_HOUR, MINUTES_IN_YEAR, SECONDS_IN_MINUTE};
/** /**
* @internal * @internal
*/ */
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\loadConfig')]
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\createPlaceholderImage')]
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\renderTemplate')]
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\getLocalImg')]
final class AnimeClientTest extends AnimeClientTestCase final class AnimeClientTest extends AnimeClientTestCase
{ {
public function testArrayToToml(): void public function testArrayToToml(): void
@ -128,4 +134,33 @@ final class AnimeClientTest extends AnimeClientTestCase
{ {
$this->assertTrue(clearCache($this->container->get('cache'))); $this->assertTrue(clearCache($this->container->get('cache')));
} }
public static function getFriendlyTime(): array
{
$SECONDS_IN_DAY = SECONDS_IN_MINUTE * MINUTES_IN_DAY;
$SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
$SECONDS_IN_YEAR = SECONDS_IN_MINUTE * MINUTES_IN_YEAR;
return [[
'seconds' => $SECONDS_IN_YEAR,
'expected' => '1 year',
], [
'seconds' => $SECONDS_IN_HOUR,
'expected' => '1 hour',
], [
'seconds' => (2 * $SECONDS_IN_YEAR) + 30,
'expected' => '2 years, 30 seconds',
], [
'seconds' => (5 * $SECONDS_IN_YEAR) + (3 * $SECONDS_IN_DAY) + (17 * SECONDS_IN_MINUTE),
'expected' => '5 years, 3 days, and 17 minutes',
]];
}
#[\PHPUnit\Framework\Attributes\DataProvider('getFriendlyTime')]
public function testGetFriendlyTime(int $seconds, string $expected): void
{
$actual = friendlyTime($seconds);
$this->assertSame($expected, $actual);
}
} }

View File

@ -40,10 +40,10 @@ class AnimeClientTestCase extends TestCase
use MatchesSnapshots; use MatchesSnapshots;
// Test directory constants // Test directory constants
public const ROOT_DIR = AC_TEST_ROOT_DIR; final public const ROOT_DIR = AC_TEST_ROOT_DIR;
public const SRC_DIR = SRC_DIR; final public const SRC_DIR = SRC_DIR;
public const TEST_DATA_DIR = __DIR__ . '/test_data'; final public const TEST_DATA_DIR = __DIR__ . '/test_data';
public const TEST_VIEW_DIR = __DIR__ . '/test_views'; final public const TEST_VIEW_DIR = __DIR__ . '/test_views';
protected ContainerInterface $container; protected ContainerInterface $container;
@ -97,7 +97,7 @@ class AnimeClientTestCase extends TestCase
$container = $di($config_array); $container = $di($config_array);
// Use mock session handler // Use mock session handler
$container->set('session-handler', static function () { $container->set('session-handler', static function (): TestSessionHandler {
$session_handler = new TestSessionHandler(); $session_handler = new TestSessionHandler();
session_set_save_handler($session_handler, TRUE); session_set_save_handler($session_handler, TRUE);
@ -123,7 +123,7 @@ class AnimeClientTestCase extends TestCase
]; ];
$request = call_user_func_array( $request = call_user_func_array(
[ServerRequestFactory::class, 'fromGlobals'], ServerRequestFactory::fromGlobals(...),
array_values(array_merge($default, $supers)), array_values(array_merge($default, $supers)),
); );
$this->container->setInstance('request', $request); $this->container->setInstance('request', $request);

Some files were not shown because too many files have changed in this diff Show More