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,6 +3,7 @@ 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'], [
@ -11,32 +12,49 @@ use Aviat\AnimeClient\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(
$character['names']['canonical'],
$url->generate('character', ['slug' => $character['slug']]), $url->generate('character', ['slug' => $character['slug']]),
$character['names']['canonical'] $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"/>
@ -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) => {
try {
const resData = JSON.parse(res); 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 // We've completed the series
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') { if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
_.hide(parentSel); _.hide(parentSel);
_.hide('#loading-shadow'); displayMessage('success', 'Completed')
_.showMessage('success', `Successfully completed ${title}`);
_.scrollToTop();
return; return;
} }
_.hide('#loading-shadow'); // Just a normal update
_.showMessage('success', `Successfully updated ${title}`);
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount; _.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
_.scrollToTop(); displayMessage('success', 'Updated');
}, } catch (_) {
error: () => { showError();
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
} }
},
error: showError,
}); });
}); });

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');
if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
showError();
return; return;
} }
if (String(data.data.status).toUpperCase() === 'COMPLETED') { // We've completed the series
if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
_.hide(parentSel); _.hide(parentSel);
_.hide('#loading-shadow'); displayMessage('success', 'Completed')
_.showMessage('success', `Successfully completed ${mangaName}`);
_.scrollToTop();
return; return;
} }
_.hide('#loading-shadow'); // Just a normal update
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed); _.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
_.showMessage('success', `Successfully updated ${mangaName}`); displayMessage('success', 'Updated');
_.scrollToTop();
}, } catch (_) {
error: () => { showError();
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${mangaName}`);
_.scrollToTop();
} }
},
error: showError,
}); });
}); });

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 {

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
*/ */
@ -123,10 +123,10 @@ class Controller
/** /**
* 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();
@ -163,9 +163,9 @@ class Controller
* *
* 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') ?? '/';
@ -176,8 +176,8 @@ class Controller
/** /**
* 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())
@ -192,9 +192,8 @@ class Controller
/** /**
* 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');
@ -219,9 +218,8 @@ class Controller
/** /**
* 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 = [
@ -247,13 +245,13 @@ class Controller
/** /**
* 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,
@ -265,9 +263,9 @@ class Controller
/** /**
* 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', [
@ -280,9 +278,9 @@ class Controller
/** /**
* 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');
@ -292,9 +290,8 @@ class Controller
/** /**
* 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;
@ -325,9 +322,9 @@ class Controller
/** /**
* 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', [
@ -339,9 +336,9 @@ class Controller
/** /**
* 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)
@ -356,10 +353,10 @@ class Controller
/** /**
* 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()
@ -370,9 +367,8 @@ class Controller
/** /**
* 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()

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,7 +307,8 @@ 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,
@ -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

@ -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,10 +201,8 @@ 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;

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

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

View File

@ -21,24 +21,36 @@ use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use IteratorAggregate; use IteratorAggregate;
use OutOfBoundsException; use OutOfBoundsException;
use RuntimeException;
use Traversable; use Traversable;
use function mb_convert_case;
use function mb_ereg_match;
use function mb_ereg_replace;
use function mb_internal_encoding;
use function mb_regex_encoding;
use function mb_split;
use function mb_stripos;
use function mb_strlen;
use function mb_strrpos;
use function mb_strtolower;
use function mb_strtoupper;
use function mb_substr;
use function mb_substr_count;
use const MB_CASE_TITLE;
/** /**
* Vendored, slightly modernized version of Stringy * Vendored, slightly modernized version of Stringy
*/ */
abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess { abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess
{
/** /**
* An instance's string. * An instance's string.
*
* @var string
*/ */
protected string $str; protected string $str;
/** /**
* The string's encoding, which should be one of the mbstring module's * The string's encoding, which should be one of the mbstring module's
* supported encodings. * supported encodings.
*
* @var string
*/ */
protected string $encoding; protected string $encoding;
@ -51,25 +63,36 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* *
* @param mixed $str Value to modify, after being cast to string * @param mixed $str Value to modify, after being cast to string
* @param string|null $encoding The character encoding * @param string|null $encoding The character encoding
* @throws \InvalidArgumentException if an array or object without a * @throws InvalidArgumentException if an array or object without a
* __toString method is passed as the first argument * __toString method is passed as the first argument
*/ */
public function __construct(mixed $str = '', ?string $encoding = NULL) final public function __construct(mixed $str = '', ?string $encoding = NULL)
{ {
if (is_array($str)) if (is_array($str))
{ {
throw new InvalidArgumentException( throw new InvalidArgumentException(
'Passed value cannot be an array' 'Passed value cannot be an array'
); );
} elseif (is_object($str) && ! method_exists($str, '__toString')) }
if (is_object($str) && ! method_exists($str, '__toString'))
{ {
throw new InvalidArgumentException( throw new InvalidArgumentException(
'Passed object must have a __toString method' 'Passed object must have a __toString method'
); );
} }
$this->str = (string)$str; $this->str = (string) $str;
$this->encoding = $encoding ?: \mb_internal_encoding(); $this->encoding = $encoding ?: mb_internal_encoding();
}
/**
* Returns the value in $str.
*
* @return string The current value of the $str property
*/
public function __toString(): string
{
return $this->str;
} }
/** /**
@ -81,25 +104,15 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* *
* @param mixed $str Value to modify, after being cast to string * @param mixed $str Value to modify, after being cast to string
* @param string|null $encoding The character encoding * @param string|null $encoding The character encoding
* @return static A Stringy object * @throws InvalidArgumentException if an array or object without a
* @throws \InvalidArgumentException if an array or object without a
* __toString method is passed as the first argument * __toString method is passed as the first argument
* @return static A Stringy object
*/ */
public static function create(mixed $str = '', ?string $encoding = NULL): self public static function create(mixed $str = '', ?string $encoding = NULL): self
{ {
return new static($str, $encoding); return new static($str, $encoding);
} }
/**
* Returns the value in $str.
*
* @return string The current value of the $str property
*/
public function __toString(): string
{
return $this->str;
}
/** /**
* Returns a new string with $string appended. * Returns a new string with $string appended.
* *
@ -140,7 +153,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
return static::create('', $this->encoding); return static::create('', $this->encoding);
} }
$substrIndex = $startIndex + \mb_strlen($start, $this->encoding); $substrIndex = $startIndex + mb_strlen($start, $this->encoding);
$endIndex = $this->indexOf($end, $substrIndex); $endIndex = $this->indexOf($end, $substrIndex);
if ($endIndex === FALSE) if ($endIndex === FALSE)
{ {
@ -165,10 +178,10 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$stringy->str = preg_replace_callback( $stringy->str = preg_replace_callback(
'/[-_\s]+(.)?/u', '/[-_\s]+(.)?/u',
function ($match) use ($encoding) { static function ($match) use ($encoding): string {
if (isset($match[1])) if (isset($match[1]))
{ {
return \mb_strtoupper($match[1], $encoding); return mb_strtoupper($match[1], $encoding);
} }
return ''; return '';
@ -178,9 +191,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$stringy->str = preg_replace_callback( $stringy->str = preg_replace_callback(
'/[\d]+(.)?/u', '/[\d]+(.)?/u',
function ($match) use ($encoding) { static fn ($match) => mb_strtoupper($match[0], $encoding),
return \mb_strtoupper($match[0], $encoding);
},
$stringy->str $stringy->str
); );
@ -195,6 +206,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
public function chars(): array public function chars(): array
{ {
$chars = []; $chars = [];
for ($i = 0, $l = $this->length(); $i < $l; $i++) for ($i = 0, $l = $this->length(); $i < $l; $i++)
{ {
$chars[] = $this->at($i)->str; $chars[] = $this->at($i)->str;
@ -230,10 +242,10 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($caseSensitive) if ($caseSensitive)
{ {
return (\mb_strpos($this->str, $needle, 0, $encoding) !== FALSE); return \mb_strpos($this->str, $needle, 0, $encoding) !== FALSE;
} }
return (\mb_stripos($this->str, $needle, 0, $encoding) !== FALSE); return mb_stripos($this->str, $needle, 0, $encoding) !== FALSE;
} }
/** /**
@ -313,13 +325,13 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
{ {
if ($caseSensitive) if ($caseSensitive)
{ {
return \mb_substr_count($this->str, $substring, $this->encoding); return mb_substr_count($this->str, $substring, $this->encoding);
} }
$str = \mb_strtoupper($this->str, $this->encoding); $str = mb_strtoupper($this->str, $this->encoding);
$substring = \mb_strtoupper($substring, $this->encoding); $substring = mb_strtoupper($substring, $this->encoding);
return \mb_substr_count($str, $substring, $this->encoding); return mb_substr_count($str, $substring, $this->encoding);
} }
/** /**
@ -349,7 +361,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$this->regexEncoding($this->encoding); $this->regexEncoding($this->encoding);
$str = $this->eregReplace('\B([A-Z])', '-\1', $this->trim()->__toString()); $str = $this->eregReplace('\B([A-Z])', '-\1', $this->trim()->__toString());
$str = \mb_strtolower($str, $this->encoding); $str = mb_strtolower($str, $this->encoding);
$str = $this->eregReplace('[-_\s]+', $delimiter, $str); $str = $this->eregReplace('[-_\s]+', $delimiter, $str);
$this->regexEncoding($regexEncoding); $this->regexEncoding($regexEncoding);
@ -368,19 +380,23 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function endsWith(string $substring, bool $caseSensitive = TRUE): bool public function endsWith(string $substring, bool $caseSensitive = TRUE): bool
{ {
$substringLength = \mb_strlen($substring, $this->encoding); $substringLength = mb_strlen($substring, $this->encoding);
$strLength = $this->length(); $strLength = $this->length();
$endOfStr = \mb_substr($this->str, $strLength - $substringLength, $endOfStr = mb_substr(
$substringLength, $this->encoding); $this->str,
$strLength - $substringLength,
$substringLength,
$this->encoding
);
if ( ! $caseSensitive) if ( ! $caseSensitive)
{ {
$substring = \mb_strtolower($substring, $this->encoding); $substring = mb_strtolower($substring, $this->encoding);
$endOfStr = \mb_strtolower($endOfStr, $this->encoding); $endOfStr = mb_strtolower($endOfStr, $this->encoding);
} }
return (string)$substring === $endOfStr; return (string) $substring === $endOfStr;
} }
/** /**
@ -462,6 +478,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($n < 0) if ($n < 0)
{ {
$stringy->str = ''; $stringy->str = '';
return $stringy; return $stringy;
} }
@ -513,7 +530,6 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
return $this->matchesPattern('.*[[:upper:]]'); return $this->matchesPattern('.*[[:upper:]]');
} }
/** /**
* Convert all HTML entities to their applicable characters. An alias of * Convert all HTML entities to their applicable characters. An alias of
* html_entity_decode. For a list of flags, refer to * html_entity_decode. For a list of flags, refer to
@ -564,12 +580,16 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* *
* @param string $needle Substring to look for * @param string $needle Substring to look for
* @param int $offset Offset from which to search * @param int $offset Offset from which to search
* @return int|bool The occurrence's index if found, otherwise false * @return bool|int The occurrence's index if found, otherwise false
*/ */
public function indexOf(string $needle, int $offset = 0): int|false public function indexOf(string $needle, int $offset = 0): int|false
{ {
return \mb_strpos($this->str, (string)$needle, return \mb_strpos(
(int)$offset, $this->encoding); $this->str,
(string) $needle,
(int) $offset,
$this->encoding
);
} }
/** /**
@ -580,12 +600,16 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* *
* @param string $needle Substring to look for * @param string $needle Substring to look for
* @param int $offset Offset from which to search * @param int $offset Offset from which to search
* @return int|bool The last occurrence's index if found, otherwise false * @return bool|int The last occurrence's index if found, otherwise false
*/ */
public function indexOfLast(string $needle, int $offset = 0): int|false public function indexOfLast(string $needle, int $offset = 0): int|false
{ {
return \mb_strrpos($this->str, (string)$needle, return mb_strrpos(
(int)$offset, $this->encoding); $this->str,
(string) $needle,
(int) $offset,
$this->encoding
);
} }
/** /**
@ -603,9 +627,13 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
return $stringy; return $stringy;
} }
$start = \mb_substr($stringy->str, 0, $index, $stringy->encoding); $start = mb_substr($stringy->str, 0, $index, $stringy->encoding);
$end = \mb_substr($stringy->str, $index, $stringy->length(), $end = mb_substr(
$stringy->encoding); $stringy->str,
$index,
$stringy->length(),
$stringy->encoding
);
$stringy->str = $start . $substring . $end; $stringy->str = $start . $substring . $end;
@ -672,7 +700,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
json_decode($this->str); json_decode($this->str);
return (json_last_error() === JSON_ERROR_NONE); return json_last_error() === JSON_ERROR_NONE;
} }
/** /**
@ -696,7 +724,6 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
return $this->str === 'b:0;' || @unserialize($this->str) !== FALSE; return $this->str === 'b:0;' || @unserialize($this->str) !== FALSE;
} }
/** /**
* Returns true if the string is base64 encoded, false otherwise. * Returns true if the string is base64 encoded, false otherwise.
* *
@ -704,7 +731,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function isBase64(): bool public function isBase64(): bool
{ {
return (base64_encode(base64_decode($this->str, TRUE)) === $this->str); return base64_encode(base64_decode($this->str, TRUE)) === $this->str;
} }
/** /**
@ -731,6 +758,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($n <= 0) if ($n <= 0)
{ {
$stringy->str = ''; $stringy->str = '';
return $stringy; return $stringy;
} }
@ -744,7 +772,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function length(): int public function length(): int
{ {
return \mb_strlen($this->str, $this->encoding); return mb_strlen($this->str, $this->encoding);
} }
/** /**
@ -756,7 +784,9 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
public function lines(): array public function lines(): array
{ {
$array = $this->split('[\r\n]{1,2}', $this->str); $array = $this->split('[\r\n]{1,2}', $this->str);
for ($i = 0; $i < count($array); $i++) $arrayCount = count($array);
for ($i = 0; $i < $arrayCount; $i++)
{ {
$array[$i] = static::create($array[$i], $this->encoding); $array[$i] = static::create($array[$i], $this->encoding);
} }
@ -773,17 +803,19 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
public function longestCommonPrefix(string $otherStr): self public function longestCommonPrefix(string $otherStr): self
{ {
$encoding = $this->encoding; $encoding = $this->encoding;
$maxLength = min($this->length(), \mb_strlen($otherStr, $encoding)); $maxLength = min($this->length(), mb_strlen($otherStr, $encoding));
$longestCommonPrefix = ''; $longestCommonPrefix = '';
for ($i = 0; $i < $maxLength; $i++) for ($i = 0; $i < $maxLength; $i++)
{ {
$char = \mb_substr($this->str, $i, 1, $encoding); $char = mb_substr($this->str, $i, 1, $encoding);
if ($char == \mb_substr($otherStr, $i, 1, $encoding)) if ($char === mb_substr($otherStr, $i, 1, $encoding))
{ {
$longestCommonPrefix .= $char; $longestCommonPrefix .= $char;
} else }
else
{ {
break; break;
} }
@ -801,17 +833,19 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
public function longestCommonSuffix(string $otherStr): self public function longestCommonSuffix(string $otherStr): self
{ {
$encoding = $this->encoding; $encoding = $this->encoding;
$maxLength = min($this->length(), \mb_strlen($otherStr, $encoding)); $maxLength = min($this->length(), mb_strlen($otherStr, $encoding));
$longestCommonSuffix = ''; $longestCommonSuffix = '';
for ($i = 1; $i <= $maxLength; $i++) for ($i = 1; $i <= $maxLength; $i++)
{ {
$char = \mb_substr($this->str, -$i, 1, $encoding); $char = mb_substr($this->str, -$i, 1, $encoding);
if ($char == \mb_substr($otherStr, -$i, 1, $encoding)) if ($char === mb_substr($otherStr, -$i, 1, $encoding))
{ {
$longestCommonSuffix = $char . $longestCommonSuffix; $longestCommonSuffix = $char . $longestCommonSuffix;
} else }
else
{ {
break; break;
} }
@ -834,28 +868,32 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$encoding = $this->encoding; $encoding = $this->encoding;
$stringy = static::create($this->str, $encoding); $stringy = static::create($this->str, $encoding);
$strLength = $stringy->length(); $strLength = $stringy->length();
$otherLength = \mb_strlen($otherStr, $encoding); $otherLength = mb_strlen($otherStr, $encoding);
// Return if either string is empty // Return if either string is empty
if ($strLength == 0 || $otherLength == 0) if ($strLength === 0 || $otherLength === 0)
{ {
$stringy->str = ''; $stringy->str = '';
return $stringy; return $stringy;
} }
$len = 0; $len = 0;
$end = 0; $end = 0;
$table = array_fill(0, $strLength + 1, $table = array_fill(
array_fill(0, $otherLength + 1, 0)); 0,
$strLength + 1,
array_fill(0, $otherLength + 1, 0)
);
for ($i = 1; $i <= $strLength; $i++) for ($i = 1; $i <= $strLength; $i++)
{ {
for ($j = 1; $j <= $otherLength; $j++) for ($j = 1; $j <= $otherLength; $j++)
{ {
$strChar = \mb_substr($stringy->str, $i - 1, 1, $encoding); $strChar = mb_substr($stringy->str, $i - 1, 1, $encoding);
$otherChar = \mb_substr($otherStr, $j - 1, 1, $encoding); $otherChar = mb_substr($otherStr, $j - 1, 1, $encoding);
if ($strChar == $otherChar) if ($strChar === $otherChar)
{ {
$table[$i][$j] = $table[$i - 1][$j - 1] + 1; $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
if ($table[$i][$j] > $len) if ($table[$i][$j] > $len)
@ -863,14 +901,15 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$len = $table[$i][$j]; $len = $table[$i][$j];
$end = $i; $end = $i;
} }
} else }
else
{ {
$table[$i][$j] = 0; $table[$i][$j] = 0;
} }
} }
} }
$stringy->str = \mb_substr($stringy->str, $end - $len, $len, $encoding); $stringy->str = mb_substr($stringy->str, $end - $len, $len, $encoding);
return $stringy; return $stringy;
} }
@ -882,11 +921,15 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function lowerCaseFirst(): self public function lowerCaseFirst(): self
{ {
$first = \mb_substr($this->str, 0, 1, $this->encoding); $first = mb_substr($this->str, 0, 1, $this->encoding);
$rest = \mb_substr($this->str, 1, $this->length() - 1, $rest = mb_substr(
$this->encoding); $this->str,
1,
$this->length() - 1,
$this->encoding
);
$str = \mb_strtolower($first, $this->encoding) . $rest; $str = mb_strtolower($first, $this->encoding) . $rest;
return static::create($str, $this->encoding); return static::create($str, $this->encoding);
} }
@ -897,19 +940,19 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* part of the ArrayAccess interface. * part of the ArrayAccess interface.
* *
* @param mixed $offset The index to check * @param mixed $offset The index to check
* @return boolean Whether or not the index exists * @return bool Whether or not the index exists
*/ */
public function offsetExists(mixed $offset): bool public function offsetExists(mixed $offset): bool
{ {
$length = $this->length(); $length = $this->length();
$offset = (int)$offset; $offset = (int) $offset;
if ($offset >= 0) if ($offset >= 0)
{ {
return ($length > $offset); return $length > $offset;
} }
return ($length >= abs($offset)); return $length >= abs($offset);
} }
/** /**
@ -919,13 +962,13 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* does not exist. * does not exist.
* *
* @param mixed $offset The index from which to retrieve the char * @param mixed $offset The index from which to retrieve the char
* @return string The character at the specified index * @throws OutOfBoundsException If the positive or negative offset does
* @throws \OutOfBoundsException If the positive or negative offset does
* not exist * not exist
* @return string The character at the specified index
*/ */
public function offsetGet(mixed $offset): string public function offsetGet(mixed $offset): string
{ {
$offset = (int)$offset; $offset = (int) $offset;
$length = $this->length(); $length = $this->length();
if (($offset >= 0 && $length <= $offset) || $length < abs($offset)) if (($offset >= 0 && $length <= $offset) || $length < abs($offset))
@ -933,7 +976,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
throw new OutOfBoundsException('No character exists at the index'); throw new OutOfBoundsException('No character exists at the index');
} }
return \mb_substr($this->str, $offset, 1, $this->encoding); return mb_substr($this->str, $offset, 1, $this->encoding);
} }
/** /**
@ -942,7 +985,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* *
* @param mixed $offset The index of the character * @param mixed $offset The index of the character
* @param mixed $value Value to set * @param mixed $value Value to set
* @throws \Exception When called * @throws Exception When called
*/ */
public function offsetSet(mixed $offset, mixed $value): void public function offsetSet(mixed $offset, mixed $value): void
{ {
@ -955,7 +998,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* when called. This maintains the immutability of Stringy objects. * when called. This maintains the immutability of Stringy objects.
* *
* @param mixed $offset The index of the character * @param mixed $offset The index of the character
* @throws \Exception When called * @throws Exception When called
*/ */
public function offsetUnset(mixed $offset): void public function offsetUnset(mixed $offset): void
{ {
@ -973,13 +1016,13 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* @param int $length Desired string length after padding * @param int $length Desired string length after padding
* @param string $padStr String used to pad, defaults to space * @param string $padStr String used to pad, defaults to space
* @param string $padType One of 'left', 'right', 'both' * @param string $padType One of 'left', 'right', 'both'
* @return static Object with a padded $str
* @throws /InvalidArgumentException If $padType isn't one of 'right', * @throws /InvalidArgumentException If $padType isn't one of 'right',
* 'left' or 'both' * 'left' or 'both'
* @return static Object with a padded $str
*/ */
public function pad(int $length, string $padStr = ' ', string $padType = 'right'): self public function pad(int $length, string $padStr = ' ', string $padType = 'right'): self
{ {
if ( ! in_array($padType, ['left', 'right', 'both'])) if ( ! in_array($padType, ['left', 'right', 'both'], TRUE))
{ {
throw new InvalidArgumentException('Pad expects $padType ' . throw new InvalidArgumentException('Pad expects $padType ' .
"to be one of 'left', 'right' or 'both'"); "to be one of 'left', 'right' or 'both'");
@ -989,8 +1032,10 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
{ {
case 'left': case 'left':
return $this->padLeft($length, $padStr); return $this->padLeft($length, $padStr);
case 'right': case 'right':
return $this->padRight($length, $padStr); return $this->padRight($length, $padStr);
default: default:
return $this->padBoth($length, $padStr); return $this->padBoth($length, $padStr);
} }
@ -1008,8 +1053,11 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
{ {
$padding = $length - $this->length(); $padding = $length - $this->length();
return $this->applyPadding(floor($padding / 2), ceil($padding / 2), return $this->applyPadding(
$padStr); floor($padding / 2),
ceil($padding / 2),
$padStr
);
} }
/** /**
@ -1084,7 +1132,8 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($stringy->startsWith($substring)) if ($stringy->startsWith($substring))
{ {
$substringLength = \mb_strlen($substring, $stringy->encoding); $substringLength = mb_strlen($substring, $stringy->encoding);
return $stringy->substr($substringLength); return $stringy->substr($substringLength);
} }
@ -1103,7 +1152,8 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($stringy->endsWith($substring)) if ($stringy->endsWith($substring))
{ {
$substringLength = \mb_strlen($substring, $stringy->encoding); $substringLength = mb_strlen($substring, $stringy->encoding);
return $stringy->substr(0, $stringy->length() - $substringLength); return $stringy->substr(0, $stringy->length() - $substringLength);
} }
@ -1148,7 +1198,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
// Loop from last index of string to first // Loop from last index of string to first
for ($i = $strLength - 1; $i >= 0; $i--) for ($i = $strLength - 1; $i >= 0; $i--)
{ {
$reversed .= \mb_substr($this->str, $i, 1, $this->encoding); $reversed .= mb_substr($this->str, $i, 1, $this->encoding);
} }
return static::create($reversed, $this->encoding); return static::create($reversed, $this->encoding);
@ -1174,19 +1224,19 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
// Need to further trim the string so we can append the substring // Need to further trim the string so we can append the substring
$encoding = $stringy->encoding; $encoding = $stringy->encoding;
$substringLength = \mb_strlen($substring, $encoding); $substringLength = mb_strlen($substring, $encoding);
$length = $length - $substringLength; $length = $length - $substringLength;
$truncated = \mb_substr($stringy->str, 0, $length, $encoding); $truncated = mb_substr($stringy->str, 0, $length, $encoding);
// If the last word was truncated // If the last word was truncated
if (mb_strpos($stringy->str, ' ', $length - 1, $encoding) != $length) if (mb_strpos($stringy->str, ' ', $length - 1, $encoding) !== $length)
{ {
// Find pos of the last occurrence of a space, get up to that // Find pos of the last occurrence of a space, get up to that
$lastPos = \mb_strrpos($truncated, ' ', 0, $encoding); $lastPos = mb_strrpos($truncated, ' ', 0, $encoding);
if ($lastPos !== FALSE) if ($lastPos !== FALSE)
{ {
$truncated = \mb_substr($truncated, 0, $lastPos, $encoding); $truncated = mb_substr($truncated, 0, $lastPos, $encoding);
} }
} }
@ -1195,7 +1245,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
return $stringy; return $stringy;
} }
/* /**
* A multibyte str_shuffle() function. It returns a string with its * A multibyte str_shuffle() function. It returns a string with its
* characters in random order. * characters in random order.
* *
@ -1207,9 +1257,10 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
shuffle($indexes); shuffle($indexes);
$shuffledStr = ''; $shuffledStr = '';
foreach ($indexes as $i) foreach ($indexes as $i)
{ {
$shuffledStr .= \mb_substr($this->str, $i, 1, $this->encoding); $shuffledStr .= mb_substr($this->str, $i, 1, $this->encoding);
} }
return static::create($shuffledStr, $this->encoding); return static::create($shuffledStr, $this->encoding);
@ -1233,7 +1284,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$stringy->str = str_replace('@', $replacement, $stringy); $stringy->str = str_replace('@', $replacement, $stringy);
$quotedReplacement = preg_quote($replacement); $quotedReplacement = preg_quote($replacement);
$pattern = "/[^a-zA-Z\d\s-_$quotedReplacement]/u"; $pattern = "/[^a-zA-Z\\d\\s-_{$quotedReplacement}]/u";
$stringy->str = preg_replace($pattern, '', $stringy); $stringy->str = preg_replace($pattern, '', $stringy);
return $stringy->toLowerCase()->delimit($replacement) return $stringy->toLowerCase()->delimit($replacement)
@ -1252,17 +1303,21 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function startsWith(string $substring, bool $caseSensitive = TRUE): bool public function startsWith(string $substring, bool $caseSensitive = TRUE): bool
{ {
$substringLength = \mb_strlen($substring, $this->encoding); $substringLength = mb_strlen($substring, $this->encoding);
$startOfStr = \mb_substr($this->str, 0, $substringLength, $startOfStr = mb_substr(
$this->encoding); $this->str,
0,
$substringLength,
$this->encoding
);
if ( ! $caseSensitive) if ( ! $caseSensitive)
{ {
$substring = \mb_strtolower($substring, $this->encoding); $substring = mb_strtolower($substring, $this->encoding);
$startOfStr = \mb_strtolower($startOfStr, $this->encoding); $startOfStr = mb_strtolower($startOfStr, $this->encoding);
} }
return (string)$substring === $startOfStr; return (string) $substring === $startOfStr;
} }
/** /**
@ -1303,18 +1358,21 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* @param int $end Optional index at which to end extraction * @param int $end Optional index at which to end extraction
* @return static Object with its $str being the extracted substring * @return static Object with its $str being the extracted substring
*/ */
public function slice(int $start, int $end = NULL): self public function slice(int $start, ?int $end = NULL): self
{ {
if ($end === NULL) if ($end === NULL)
{ {
$length = $this->length(); $length = $this->length();
} elseif ($end >= 0 && $end <= $start) }
elseif ($end >= 0 && $end <= $start)
{ {
return static::create('', $this->encoding); return static::create('', $this->encoding);
} elseif ($end < 0) }
elseif ($end < 0)
{ {
$length = $this->length() + $end - $start; $length = $this->length() + $end - $start;
} else }
else
{ {
$length = $end - $start; $length = $end - $start;
} }
@ -1331,7 +1389,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
* @param int $limit Optional maximum number of results to return * @param int $limit Optional maximum number of results to return
* @return static[] An array of Stringy objects * @return static[] An array of Stringy objects
*/ */
public function split(string $pattern, int $limit = NULL): array public function split(string $pattern, ?int $limit = NULL): array
{ {
if ($limit === 0) if ($limit === 0)
{ {
@ -1350,7 +1408,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
// mb_split returns the remaining unsplit string in the last index when // mb_split returns the remaining unsplit string in the last index when
// supplying a limit // supplying a limit
$limit = ($limit > 0) ? $limit += 1 : -1; $limit = ($limit > 0) ? ++$limit : -1;
static $functionExists; static $functionExists;
if ($functionExists === NULL) if ($functionExists === NULL)
@ -1360,10 +1418,11 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($functionExists) if ($functionExists)
{ {
$array = \mb_split($pattern, $this->str, $limit); $array = mb_split($pattern, $this->str, $limit);
} else if ($this->supportsEncoding()) }
elseif ($this->supportsEncoding())
{ {
$array = \preg_split("/$pattern/", $this->str, $limit); $array = \preg_split("/{$pattern}/", $this->str, $limit);
} }
$this->regexEncoding($regexEncoding); $this->regexEncoding($regexEncoding);
@ -1372,8 +1431,9 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
{ {
array_pop($array); array_pop($array);
} }
$arrayCount = count($array);
for ($i = 0; $i < count($array); $i++) for ($i = 0; $i < $arrayCount; $i++)
{ {
$array[$i] = static::create($array[$i], $this->encoding); $array[$i] = static::create($array[$i], $this->encoding);
} }
@ -1405,7 +1465,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
public function substr(int $start, ?int $length = NULL): self public function substr(int $start, ?int $length = NULL): self
{ {
$length = $length === NULL ? $this->length() : $length; $length = $length === NULL ? $this->length() : $length;
$str = \mb_substr($this->str, $start, $length, $this->encoding); $str = mb_substr($this->str, $start, $length, $this->encoding);
return static::create($str, $this->encoding); return static::create($str, $this->encoding);
} }
@ -1436,13 +1496,13 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$stringy->str = preg_replace_callback( $stringy->str = preg_replace_callback(
'/[\S]/u', '/[\S]/u',
function ($match) use ($encoding) { static function ($match) use ($encoding): string {
if ($match[0] == \mb_strtoupper($match[0], $encoding)) if ($match[0] === mb_strtoupper($match[0], $encoding))
{ {
return \mb_strtolower($match[0], $encoding); return mb_strtolower($match[0], $encoding);
} }
return \mb_strtoupper($match[0], $encoding); return mb_strtoupper($match[0], $encoding);
}, },
$stringy->str $stringy->str
); );
@ -1489,15 +1549,15 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$stringy->str = preg_replace_callback( $stringy->str = preg_replace_callback(
'/([\S]+)/u', '/([\S]+)/u',
function ($match) use ($encoding, $ignore) { static function ($match) use ($encoding, $ignore): string {
if ($ignore && in_array($match[0], $ignore)) if ($ignore && in_array($match[0], $ignore, TRUE))
{ {
return $match[0]; return $match[0];
} }
$stringy = static::create($match[0], $encoding); $stringy = static::create($match[0], $encoding);
return (string)$stringy->toLowerCase()->upperCaseFirst(); return (string) $stringy->toLowerCase()->upperCaseFirst();
}, },
$stringy->str $stringy->str
); );
@ -1563,18 +1623,19 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
'false' => FALSE, 'false' => FALSE,
'0' => FALSE, '0' => FALSE,
'off' => FALSE, 'off' => FALSE,
'no' => FALSE 'no' => FALSE,
]; ];
if (array_key_exists($key, $map)) if (array_key_exists($key, $map))
{ {
return $map[$key]; return $map[$key];
} elseif (is_numeric($this->str)) }
if (is_numeric($this->str))
{ {
return (intval($this->str) > 0); return (int) ($this->str) > 0;
} }
return (bool)$this->regexReplace('[[:space:]]', '')->str; return (bool) $this->regexReplace('[[:space:]]', '')->str;
} }
/** /**
@ -1585,7 +1646,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function toLowerCase(): self public function toLowerCase(): self
{ {
$str = \mb_strtolower($this->str, $this->encoding); $str = mb_strtolower($this->str, $this->encoding);
return static::create($str, $this->encoding); return static::create($str, $this->encoding);
} }
@ -1628,7 +1689,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function toTitleCase(): self public function toTitleCase(): self
{ {
$str = \mb_convert_case($this->str, \MB_CASE_TITLE, $this->encoding); $str = mb_convert_case($this->str, MB_CASE_TITLE, $this->encoding);
return static::create($str, $this->encoding); return static::create($str, $this->encoding);
} }
@ -1641,7 +1702,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function toUpperCase(): self public function toUpperCase(): self
{ {
$str = \mb_strtoupper($this->str, $this->encoding); $str = mb_strtoupper($this->str, $this->encoding);
return static::create($str, $this->encoding); return static::create($str, $this->encoding);
} }
@ -1658,7 +1719,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
{ {
$chars = ($chars) ? preg_quote($chars) : '[:space:]'; $chars = ($chars) ? preg_quote($chars) : '[:space:]';
return $this->regexReplace("^[$chars]+|[$chars]+\$", ''); return $this->regexReplace("^[{$chars}]+|[{$chars}]+\$", '');
} }
/** /**
@ -1673,7 +1734,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
{ {
$chars = ($chars) ? preg_quote($chars) : '[:space:]'; $chars = ($chars) ? preg_quote($chars) : '[:space:]';
return $this->regexReplace("^[$chars]+", ''); return $this->regexReplace("^[{$chars}]+", '');
} }
/** /**
@ -1688,7 +1749,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
{ {
$chars = ($chars) ? preg_quote($chars) : '[:space:]'; $chars = ($chars) ? preg_quote($chars) : '[:space:]';
return $this->regexReplace("[$chars]+\$", ''); return $this->regexReplace("[{$chars}]+\$", '');
} }
/** /**
@ -1709,10 +1770,10 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
} }
// Need to further trim the string so we can append the substring // Need to further trim the string so we can append the substring
$substringLength = \mb_strlen($substring, $stringy->encoding); $substringLength = mb_strlen($substring, $stringy->encoding);
$length = $length - $substringLength; $length = $length - $substringLength;
$truncated = \mb_substr($stringy->str, 0, $length, $stringy->encoding); $truncated = mb_substr($stringy->str, 0, $length, $stringy->encoding);
$stringy->str = $truncated . $substring; $stringy->str = $truncated . $substring;
return $stringy; return $stringy;
@ -1750,11 +1811,15 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
*/ */
public function upperCaseFirst(): self public function upperCaseFirst(): self
{ {
$first = \mb_substr($this->str, 0, 1, $this->encoding); $first = mb_substr($this->str, 0, 1, $this->encoding);
$rest = \mb_substr($this->str, 1, $this->length() - 1, $rest = mb_substr(
$this->encoding); $this->str,
1,
$this->length() - 1,
$this->encoding
);
$str = \mb_strtoupper($first, $this->encoding) . $rest; $str = mb_strtoupper($first, $this->encoding) . $rest;
return static::create($str, $this->encoding); return static::create($str, $this->encoding);
} }
@ -1767,7 +1832,10 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
protected function charsArray(): array protected function charsArray(): array
{ {
static $charsArray; static $charsArray;
if (isset($charsArray)) return $charsArray; if (isset($charsArray))
{
return $charsArray;
}
return $charsArray = [ return $charsArray = [
'0' => ['°', '₀', '۰', ''], '0' => ['°', '₀', '۰', ''],
@ -1960,17 +2028,11 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
], ],
'bg' => [ 'bg' => [
['х', 'Х', 'щ', 'Щ', 'ъ', 'Ъ', 'ь', 'Ь'], ['х', 'Х', 'щ', 'Щ', 'ъ', 'Ъ', 'ь', 'Ь'],
['h', 'H', 'sht', 'SHT', 'a', 'А', 'y', 'Y'] ['h', 'H', 'sht', 'SHT', 'a', 'А', 'y', 'Y'],
] ],
]; ];
if (isset($languageSpecific[$language])) $charsArray[$language] = $languageSpecific[$language] ?? [];
{
$charsArray[$language] = $languageSpecific[$language];
} else
{
$charsArray[$language] = [];
}
return $charsArray[$language]; return $charsArray[$language];
} }
@ -1987,7 +2049,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
protected function applyPadding(int $left = 0, int $right = 0, string $padStr = ' '): self protected function applyPadding(int $left = 0, int $right = 0, string $padStr = ' '): self
{ {
$stringy = static::create($this->str, $this->encoding); $stringy = static::create($this->str, $this->encoding);
$length = \mb_strlen($padStr, $stringy->encoding); $length = mb_strlen($padStr, $stringy->encoding);
$strLength = $stringy->length(); $strLength = $stringy->length();
$paddedLength = $strLength + $left + $right; $paddedLength = $strLength + $left + $right;
@ -1997,10 +2059,18 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
return $stringy; return $stringy;
} }
$leftPadding = \mb_substr(str_repeat($padStr, ceil($left / $length)), 0, $leftPadding = mb_substr(
$left, $stringy->encoding); str_repeat($padStr, ceil($left / $length)),
$rightPadding = \mb_substr(str_repeat($padStr, ceil($right / $length)), 0,
0, $right, $stringy->encoding); $left,
$stringy->encoding
);
$rightPadding = mb_substr(
str_repeat($padStr, ceil($right / $length)),
0,
$right,
$stringy->encoding
);
$stringy->str = $leftPadding . $stringy->str . $rightPadding; $stringy->str = $leftPadding . $stringy->str . $rightPadding;
@ -2018,7 +2088,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
$regexEncoding = $this->regexEncoding(); $regexEncoding = $this->regexEncoding();
$this->regexEncoding($this->encoding); $this->regexEncoding($this->encoding);
$match = \mb_ereg_match($pattern, $this->str); $match = mb_ereg_match($pattern, $this->str);
$this->regexEncoding($regexEncoding); $this->regexEncoding($regexEncoding);
return $match; return $match;
@ -2038,11 +2108,13 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($functionExists) if ($functionExists)
{ {
return \mb_ereg_replace($pattern, $replacement, $string, $option); return mb_ereg_replace($pattern, $replacement, $string, $option);
} else if ($this->supportsEncoding()) }
if ($this->supportsEncoding())
{ {
$option = str_replace('r', '', (string)$option); $option = str_replace('r', '', (string) $option);
return \preg_replace("/$pattern/u$option", $replacement, $string);
return \preg_replace("/{$pattern}/u{$option}", $replacement, $string);
} }
} }
@ -2062,7 +2134,8 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if ($functionExists) if ($functionExists)
{ {
$args = func_get_args(); $args = func_get_args();
return call_user_func_array('\mb_regex_encoding', $args);
return mb_regex_encoding(...$args);
} }
} }
@ -2073,11 +2146,10 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess {
if (isset($supported[$this->encoding])) if (isset($supported[$this->encoding]))
{ {
return TRUE; return TRUE;
} else }
{
throw new \RuntimeException('Stringy method requires the ' . throw new RuntimeException('Stringy method requires the ' .
'mbstring module for encodings other than ASCII and UTF-8. ' . 'mbstring module for encodings other than ASCII and UTF-8. ' .
'Encoding used: ' . $this->encoding); 'Encoding used: ' . $this->encoding);
} }
}
} }

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,9 +87,7 @@ 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); $actual = $this->transformer->untransform($input);

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

View File

@ -71,7 +71,7 @@ final class DispatcherTest extends AnimeClientTestCase
$this->assertIsObject($this->router); $this->assertIsObject($this->router);
} }
public function dataRoute(): array public static function dataRoute(): array
{ {
$defaultConfig = [ $defaultConfig = [
'routes' => [ 'routes' => [
@ -141,14 +141,8 @@ final class DispatcherTest extends AnimeClientTestCase
return $data; return $data;
} }
/** #[\PHPUnit\Framework\Attributes\DataProvider('dataRoute')]
* @dataProvider dataRoute public function testRoute(mixed $config, mixed $controller, mixed $host, mixed $uri): void
* @param mixed $config
* @param mixed $controller
* @param mixed $host
* @param mixed $uri
*/
public function testRoute($config, $controller, $host, $uri): void
{ {
$this->doSetUp($config, $uri, $host); $this->doSetUp($config, $uri, $host);
@ -209,8 +203,8 @@ final class DispatcherTest extends AnimeClientTestCase
} }
#[ArrayShape(['controller_list_sanity_check' => 'array', 'empty_controller_list' => 'array'])] #[ArrayShape(['controller_list_sanity_check' => 'array', 'empty_controller_list' => 'array'])]
public function dataGetControllerList(): array public static function dataGetControllerList(): array
{ {
$expectedList = [ $expectedList = [
'anime' => Controller\Anime::class, 'anime' => Controller\Anime::class,
'anime-collection' => Controller\AnimeCollection::class, 'anime-collection' => Controller\AnimeCollection::class,
@ -248,11 +242,9 @@ final class DispatcherTest extends AnimeClientTestCase
'expected' => $expectedList, 'expected' => $expectedList,
], ],
]; ];
} }
/** #[\PHPUnit\Framework\Attributes\DataProvider('dataGetControllerList')]
* @dataProvider dataGetControllerList
*/
public function testGetControllerList(array $config, array $expected): void public function testGetControllerList(array $config, array $expected): void
{ {
$this->doSetUp($config, '/', 'localhost'); $this->doSetUp($config, '/', 'localhost');

View File

@ -251,7 +251,7 @@ final class FormGeneratorTest extends AnimeClientTestCase
{ {
$generator = FormGenerator::new($this->container); $generator = FormGenerator::new($this->container);
foreach (SETTINGS_MAP as $section => $fields) foreach (SETTINGS_MAP as $fields)
{ {
foreach ($fields as $name => $config) foreach ($fields as $name => $config)
{ {

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