mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2025-12-23 21:47:44 -05:00
* PHPStan 2.0 fix https://github.com/FreshRSS/FreshRSS/issues/6989 https://github.com/phpstan/phpstan/releases/tag/2.0.0 https://github.com/phpstan/phpstan/blob/2.0.x/UPGRADING.md * More * More * Done * fix i18n CLI * Restore a PHPStan Next test For work towards PHPStan Level 10 * 4 more on Level 10 * fix getTagsForEntry * API at Level 10 * More Level 10 * Finish Minz at Level 10 * Finish CLI at Level 10 * Finish Controllers at Level 10 * More Level 10 * More * Pass bleedingEdge * Clean PHPStan options and add TODOs * Level 10 for main config * More * Consitency array vs. list * Sanitize themes get_infos * Simplify TagDAO->getTagsForEntries() * Finish reportAnyTypeWideningInVarTag * Prepare checkBenevolentUnionTypes and checkImplicitMixed * Fixes * Refix * Another fix * Casing of __METHOD__ constant
360 lines
9.1 KiB
PHP
360 lines
9.1 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
class FreshRSS_StatsDAO extends Minz_ModelPdo {
|
|
|
|
public const ENTRY_COUNT_PERIOD = 30;
|
|
|
|
protected function sqlFloor(string $s): string {
|
|
return "FLOOR($s)";
|
|
}
|
|
|
|
/**
|
|
* Calculates entry repartition for all feeds and for main stream.
|
|
*
|
|
* @return array{'main_stream':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false,'all_feeds':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false}
|
|
*/
|
|
public function calculateEntryRepartition(): array {
|
|
return [
|
|
'main_stream' => $this->calculateEntryRepartitionPerFeed(null, true),
|
|
'all_feeds' => $this->calculateEntryRepartitionPerFeed(null, false),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculates entry repartition for the selection.
|
|
* The repartition includes:
|
|
* - total entries
|
|
* - read entries
|
|
* - unread entries
|
|
* - favorite entries
|
|
*
|
|
* @return array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false
|
|
*/
|
|
public function calculateEntryRepartitionPerFeed(?int $feed = null, bool $only_main = false): array|false {
|
|
$filter = '';
|
|
if ($only_main) {
|
|
$filter .= 'AND f.priority = 10';
|
|
}
|
|
if (!is_null($feed)) {
|
|
$filter .= "AND e.id_feed = {$feed}";
|
|
}
|
|
$sql = <<<SQL
|
|
SELECT COUNT(1) AS total,
|
|
COUNT(1) - SUM(e.is_read) AS count_unreads,
|
|
SUM(e.is_read) AS count_reads,
|
|
SUM(e.is_favorite) AS count_favorites
|
|
FROM `_entry` AS e, `_feed` AS f
|
|
WHERE e.id_feed = f.id
|
|
{$filter}
|
|
SQL;
|
|
$res = $this->fetchAssoc($sql);
|
|
if (is_array($res) && !empty($res[0])) {
|
|
$dao = $res[0];
|
|
/** @var array{total:int,count_unreads:int,count_reads:int,count_favorites:int} $dao */
|
|
return $dao;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculates entry count per day on a 30 days period.
|
|
* @return array<int,int>
|
|
*/
|
|
public function calculateEntryCount(): array {
|
|
$count = $this->initEntryCountArray();
|
|
$midnight = mktime(0, 0, 0) ?: 0;
|
|
$oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
|
|
|
|
// Get stats per day for the last 30 days
|
|
$sqlDay = $this->sqlFloor("(date - $midnight) / 86400");
|
|
$sql = <<<SQL
|
|
SELECT {$sqlDay} AS day,
|
|
COUNT(*) as count
|
|
FROM `_entry`
|
|
WHERE date >= {$oldest} AND date < {$midnight}
|
|
GROUP BY day
|
|
ORDER BY day ASC
|
|
SQL;
|
|
$res = $this->fetchAssoc($sql);
|
|
if (!is_array($res)) {
|
|
return [];
|
|
}
|
|
/** @var list<array{day:int,count:int}> $res */
|
|
foreach ($res as $value) {
|
|
$count[(int)($value['day'])] = (int)($value['count']);
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Initialize an array for the entry count.
|
|
* @return array<int,int>
|
|
*/
|
|
protected function initEntryCountArray(): array {
|
|
return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1);
|
|
}
|
|
|
|
/**
|
|
* Calculates the number of article per hour of the day per feed
|
|
* @return array<int,int>
|
|
*/
|
|
public function calculateEntryRepartitionPerFeedPerHour(?int $feed = null): array {
|
|
return $this->calculateEntryRepartitionPerFeedPerPeriod('%H', $feed);
|
|
}
|
|
|
|
/**
|
|
* Calculates the number of article per day of week per feed
|
|
* @return array<int,int>
|
|
*/
|
|
public function calculateEntryRepartitionPerFeedPerDayOfWeek(?int $feed = null): array {
|
|
return $this->calculateEntryRepartitionPerFeedPerPeriod('%w', $feed);
|
|
}
|
|
|
|
/**
|
|
* Calculates the number of article per month per feed
|
|
* @return array<int,int>
|
|
*/
|
|
public function calculateEntryRepartitionPerFeedPerMonth(?int $feed = null): array {
|
|
$monthRepartition = $this->calculateEntryRepartitionPerFeedPerPeriod('%m', $feed);
|
|
// cut out the 0th month (Jan=1, Dec=12)
|
|
\array_splice($monthRepartition, 0, 1);
|
|
return $monthRepartition;
|
|
}
|
|
|
|
/**
|
|
* Calculates the number of article per period per feed
|
|
* @param string $period format string to use for grouping
|
|
* @return array<int,int>
|
|
*/
|
|
protected function calculateEntryRepartitionPerFeedPerPeriod(string $period, ?int $feed = null): array {
|
|
$restrict = '';
|
|
if ($feed) {
|
|
$restrict = "WHERE e.id_feed = {$feed}";
|
|
}
|
|
$sql = <<<SQL
|
|
SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
|
|
, COUNT(1) AS count
|
|
FROM `_entry` AS e
|
|
{$restrict}
|
|
GROUP BY period
|
|
ORDER BY period ASC
|
|
SQL;
|
|
|
|
$res = $this->fetchAssoc($sql);
|
|
if ($res == false) {
|
|
return [];
|
|
}
|
|
$periodMax = match ($period) {
|
|
'%H' => 24,
|
|
'%w' => 7,
|
|
'%m' => 12,
|
|
default => 30,
|
|
};
|
|
|
|
$repartition = array_fill(0, $periodMax, 0);
|
|
foreach ($res as $value) {
|
|
$repartition[(int)$value['period']] = (int)$value['count'];
|
|
}
|
|
|
|
return $repartition;
|
|
}
|
|
|
|
/**
|
|
* Calculates the average number of article per hour per feed
|
|
*/
|
|
public function calculateEntryAveragePerFeedPerHour(?int $feed = null): float {
|
|
return $this->calculateEntryAveragePerFeedPerPeriod(1 / 24, $feed);
|
|
}
|
|
|
|
/**
|
|
* Calculates the average number of article per day of week per feed
|
|
*/
|
|
public function calculateEntryAveragePerFeedPerDayOfWeek(?int $feed = null): float {
|
|
return $this->calculateEntryAveragePerFeedPerPeriod(7, $feed);
|
|
}
|
|
|
|
/**
|
|
* Calculates the average number of article per month per feed
|
|
*/
|
|
public function calculateEntryAveragePerFeedPerMonth(?int $feed = null): float {
|
|
return $this->calculateEntryAveragePerFeedPerPeriod(30, $feed);
|
|
}
|
|
|
|
/**
|
|
* Calculates the average number of article per feed
|
|
* @param float $period number used to divide the number of day in the period
|
|
*/
|
|
protected function calculateEntryAveragePerFeedPerPeriod(float $period, ?int $feed = null): float {
|
|
$restrict = '';
|
|
if ($feed) {
|
|
$restrict = "WHERE e.id_feed = {$feed}";
|
|
}
|
|
$sql = <<<SQL
|
|
SELECT COUNT(1) AS count
|
|
, MIN(date) AS date_min
|
|
, MAX(date) AS date_max
|
|
FROM `_entry` AS e
|
|
{$restrict}
|
|
SQL;
|
|
$res = $this->fetchAssoc($sql);
|
|
if ($res == null || empty($res[0])) {
|
|
return -1.0;
|
|
}
|
|
$date_min = new \DateTime();
|
|
$date_min->setTimestamp((int)($res[0]['date_min']));
|
|
$date_max = new \DateTime();
|
|
$date_max->setTimestamp((int)($res[0]['date_max']));
|
|
$interval = $date_max->diff($date_min, true);
|
|
$interval_in_days = (float)($interval->format('%a'));
|
|
if ($interval_in_days <= 0) {
|
|
// Surely only one article.
|
|
// We will return count / (period/period) == count.
|
|
$interval_in_days = $period;
|
|
}
|
|
|
|
return (int)$res[0]['count'] / ($interval_in_days / $period);
|
|
}
|
|
|
|
/**
|
|
* Initialize an array for statistics depending on a range
|
|
* @return array<int,int>
|
|
*/
|
|
protected function initStatsArray(int $min, int $max): array {
|
|
return array_map(fn() => 0, array_flip(range($min, $max)));
|
|
}
|
|
|
|
/**
|
|
* Calculates feed count per category.
|
|
* @return list<array{'label':string,'data':int}>
|
|
*/
|
|
public function calculateFeedByCategory(): array {
|
|
$sql = <<<SQL
|
|
SELECT c.name AS label
|
|
, COUNT(f.id) AS data
|
|
FROM `_category` AS c, `_feed` AS f
|
|
WHERE c.id = f.category
|
|
GROUP BY label
|
|
ORDER BY data DESC
|
|
SQL;
|
|
/** @var list<array{'label':string,'data':int}>|null @res */
|
|
$res = $this->fetchAssoc($sql);
|
|
return $res == null ? [] : $res;
|
|
}
|
|
|
|
/**
|
|
* Calculates entry count per category.
|
|
* @return list<array{'label':string,'data':int}>
|
|
*/
|
|
public function calculateEntryByCategory(): array {
|
|
$sql = <<<SQL
|
|
SELECT c.name AS label
|
|
, COUNT(e.id) AS data
|
|
FROM `_category` AS c, `_feed` AS f, `_entry` AS e
|
|
WHERE c.id = f.category
|
|
AND f.id = e.id_feed
|
|
GROUP BY label
|
|
ORDER BY data DESC
|
|
SQL;
|
|
$res = $this->fetchAssoc($sql);
|
|
/** @var list<array{'label':string,'data':int}>|null $res */
|
|
return $res == null ? [] : $res;
|
|
}
|
|
|
|
/**
|
|
* Calculates the 10 top feeds based on their number of entries
|
|
* @return list<array{'id':int,'name':string,'category':string,'count':int}>
|
|
*/
|
|
public function calculateTopFeed(): array {
|
|
$sql = <<<SQL
|
|
SELECT f.id AS id
|
|
, MAX(f.name) AS name
|
|
, MAX(c.name) AS category
|
|
, COUNT(e.id) AS count
|
|
FROM `_category` AS c, `_feed` AS f, `_entry` AS e
|
|
WHERE c.id = f.category
|
|
AND f.id = e.id_feed
|
|
GROUP BY f.id
|
|
ORDER BY count DESC
|
|
LIMIT 10
|
|
SQL;
|
|
$res = $this->fetchAssoc($sql);
|
|
/** @var list<array{'id':int,'name':string,'category':string,'count':int}>|null $res */
|
|
if (is_array($res)) {
|
|
return $res;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Calculates the last publication date for each feed
|
|
* @return list<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>
|
|
*/
|
|
public function calculateFeedLastDate(): array {
|
|
$sql = <<<SQL
|
|
SELECT MAX(f.id) as id
|
|
, MAX(f.name) AS name
|
|
, MAX(date) AS last_date
|
|
, COUNT(*) AS nb_articles
|
|
FROM `_feed` AS f, `_entry` AS e
|
|
WHERE f.id = e.id_feed
|
|
GROUP BY f.id
|
|
ORDER BY name
|
|
SQL;
|
|
$res = $this->fetchAssoc($sql);
|
|
/** @var list<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>|null $res */
|
|
if (is_array($res)) {
|
|
return $res;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Gets days ready for graphs
|
|
* @return list<string>
|
|
*/
|
|
public function getDays(): array {
|
|
return $this->convertToTranslatedJson([
|
|
'sun',
|
|
'mon',
|
|
'tue',
|
|
'wed',
|
|
'thu',
|
|
'fri',
|
|
'sat',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Gets months ready for graphs
|
|
* @return list<string>
|
|
*/
|
|
public function getMonths(): array {
|
|
return $this->convertToTranslatedJson([
|
|
'jan',
|
|
'feb',
|
|
'mar',
|
|
'apr',
|
|
'may_',
|
|
'jun',
|
|
'jul',
|
|
'aug',
|
|
'sep',
|
|
'oct',
|
|
'nov',
|
|
'dec',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Translates array content
|
|
* @param list<string> $data
|
|
* @return list<string>
|
|
*/
|
|
private function convertToTranslatedJson(array $data = []): array {
|
|
$translated = array_map(static fn(string $a) => _t('gen.date.' . $a), $data);
|
|
|
|
return $translated;
|
|
}
|
|
}
|