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