160 lines
5.3 KiB
PHP
160 lines
5.3 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @package MokoSuiteOpenGraph
|
||
|
|
* @subpackage com_mokoog
|
||
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||
|
|
*/
|
||
|
|
|
||
|
|
namespace Joomla\Component\MokoOG\Administrator\Model;
|
||
|
|
|
||
|
|
defined('_JEXEC') or die;
|
||
|
|
|
||
|
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Read-only model providing OG tag coverage metrics for the dashboard.
|
||
|
|
*/
|
||
|
|
class DashboardModel extends BaseDatabaseModel
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* Overall coverage statistics for com_content articles.
|
||
|
|
*
|
||
|
|
* @return array{total:int, with_og:int, coverage:int, missing_title:int, missing_description:int, missing_image:int}
|
||
|
|
*/
|
||
|
|
public function getStats(): array
|
||
|
|
{
|
||
|
|
$db = $this->getDatabase();
|
||
|
|
|
||
|
|
$total = $this->countContent();
|
||
|
|
$withOg = $this->countDistinct();
|
||
|
|
$missingTitle = $this->countEmptyField('og_title');
|
||
|
|
$missingDesc = $this->countEmptyField('og_description');
|
||
|
|
$missingImage = $this->countEmptyField('og_image');
|
||
|
|
|
||
|
|
return [
|
||
|
|
'total' => $total,
|
||
|
|
'with_og' => $withOg,
|
||
|
|
'coverage' => $total > 0 ? (int) round(($withOg / $total) * 100) : 0,
|
||
|
|
'missing_title' => $missingTitle,
|
||
|
|
'missing_description' => $missingDesc,
|
||
|
|
'missing_image' => $missingImage,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Coverage broken down by content_type.
|
||
|
|
*
|
||
|
|
* @return array Rows of {content_type, total, with_title, with_image}
|
||
|
|
*/
|
||
|
|
public function getCoverageByType(): array
|
||
|
|
{
|
||
|
|
$db = $this->getDatabase();
|
||
|
|
$empty = $db->quote('');
|
||
|
|
|
||
|
|
$query = $db->getQuery(true)
|
||
|
|
->select([
|
||
|
|
$db->quoteName('content_type'),
|
||
|
|
'COUNT(*) AS ' . $db->quoteName('total'),
|
||
|
|
'SUM(CASE WHEN ' . $db->quoteName('og_title') . ' <> ' . $empty . ' THEN 1 ELSE 0 END) AS ' . $db->quoteName('with_title'),
|
||
|
|
'SUM(CASE WHEN ' . $db->quoteName('og_image') . ' <> ' . $empty . ' THEN 1 ELSE 0 END) AS ' . $db->quoteName('with_image'),
|
||
|
|
])
|
||
|
|
->from($db->quoteName('#__mokoog_tags'))
|
||
|
|
->where($db->quoteName('published') . ' = 1')
|
||
|
|
->group($db->quoteName('content_type'))
|
||
|
|
->order($db->quoteName('content_type') . ' ASC');
|
||
|
|
|
||
|
|
$db->setQuery($query);
|
||
|
|
|
||
|
|
return $db->loadObjectList() ?: [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Published articles that have no OG tag yet.
|
||
|
|
*
|
||
|
|
* @param int $limit Maximum rows to return
|
||
|
|
*
|
||
|
|
* @return array Rows of {id, title}
|
||
|
|
*/
|
||
|
|
public function getMissingArticles(int $limit = 20): array
|
||
|
|
{
|
||
|
|
$db = $this->getDatabase();
|
||
|
|
|
||
|
|
$query = $db->getQuery(true)
|
||
|
|
->select([$db->quoteName('c.id'), $db->quoteName('c.title')])
|
||
|
|
->from($db->quoteName('#__content', 'c'))
|
||
|
|
->leftJoin(
|
||
|
|
$db->quoteName('#__mokoog_tags', 't')
|
||
|
|
. ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content')
|
||
|
|
. ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id')
|
||
|
|
)
|
||
|
|
->where($db->quoteName('c.state') . ' = 1')
|
||
|
|
->where($db->quoteName('t.id') . ' IS NULL')
|
||
|
|
->order($db->quoteName('c.id') . ' DESC');
|
||
|
|
|
||
|
|
$db->setQuery($query, 0, max(1, $limit));
|
||
|
|
|
||
|
|
return $db->loadObjectList() ?: [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Count published com_content articles.
|
||
|
|
*/
|
||
|
|
private function countContent(): int
|
||
|
|
{
|
||
|
|
$db = $this->getDatabase();
|
||
|
|
$db->setQuery(
|
||
|
|
$db->getQuery(true)
|
||
|
|
->select('COUNT(*)')
|
||
|
|
->from($db->quoteName('#__content'))
|
||
|
|
->where($db->quoteName('state') . ' = 1')
|
||
|
|
);
|
||
|
|
|
||
|
|
return (int) $db->loadResult();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Count distinct articles that have at least one published OG tag.
|
||
|
|
*/
|
||
|
|
private function countDistinct(): int
|
||
|
|
{
|
||
|
|
$db = $this->getDatabase();
|
||
|
|
$db->setQuery(
|
||
|
|
$db->getQuery(true)
|
||
|
|
->select('COUNT(DISTINCT ' . $db->quoteName('content_id') . ')')
|
||
|
|
->from($db->quoteName('#__mokoog_tags'))
|
||
|
|
->where($db->quoteName('content_type') . ' = ' . $db->quote('com_content'))
|
||
|
|
->where($db->quoteName('published') . ' = 1')
|
||
|
|
);
|
||
|
|
|
||
|
|
return (int) $db->loadResult();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Count published OG tag rows whose given field is empty.
|
||
|
|
*
|
||
|
|
* @param string $field One of og_title, og_description, og_image
|
||
|
|
*/
|
||
|
|
private function countEmptyField(string $field): int
|
||
|
|
{
|
||
|
|
// Whitelist the column name — it is never user input here, but keep it strict.
|
||
|
|
if (!\in_array($field, ['og_title', 'og_description', 'og_image'], true)) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = $this->getDatabase();
|
||
|
|
$db->setQuery(
|
||
|
|
$db->getQuery(true)
|
||
|
|
->select('COUNT(*)')
|
||
|
|
->from($db->quoteName('#__mokoog_tags'))
|
||
|
|
->where($db->quoteName('content_type') . ' = ' . $db->quote('com_content'))
|
||
|
|
->where($db->quoteName('published') . ' = 1')
|
||
|
|
->where($db->quoteName($field) . ' = ' . $db->quote(''))
|
||
|
|
);
|
||
|
|
|
||
|
|
return (int) $db->loadResult();
|
||
|
|
}
|
||
|
|
}
|