Commit d4888794 authored by gbyte.co's avatar gbyte.co

Refactor all classes into services, refactor Batch class and remove most static methods from it

parent 62b049f1
......@@ -4,7 +4,7 @@
* Main module file containing hooks.
*/
use Drupal\simple_sitemap\Form;
use Drupal\simple_sitemap\Form\Form;
/**
* Implements hook_help.
......@@ -58,15 +58,16 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) {
*/
function simple_sitemap_entity_form_submit($form, &$form_state) {
$f = \Drupal::service('simple_sitemap.form')->processForm($form_state);
$values = $form_state->getValues();
// Fix for values appearing in a sub array on a commerce product entity.
$values = isset($values['simple_sitemap']) ? $values['simple_sitemap'] : $values;
// Only make changes in DB if sitemap settings actually changed.
if (Form::valuesChanged($form, $values)) {
if ($f->valuesChanged($form, $values)) {
$generator = \Drupal::service('simple_sitemap.generator');
$f = \Drupal::service('simple_sitemap.form')->processForm($form_state);
switch ($f->entityCategory) {
......
services:
simple_sitemap.generator:
class: Drupal\simple_sitemap\Simplesitemap
arguments: ['@config.factory', '@database', '@entity_type.manager']
arguments: ['@simple_sitemap.sitemap_generator', '@config.factory', '@database', '@entity_type.manager', '@path.validator']
simple_sitemap.sitemap_generator:
class: Drupal\simple_sitemap\SitemapGenerator
arguments: ['@simple_sitemap.generator', '@database', '@language_manager', '@module_handler']
arguments: ['@simple_sitemap.batch', '@database', '@module_handler', '@language_manager']
simple_sitemap.form:
class: Drupal\simple_sitemap\Form
arguments: ['@simple_sitemap.generator']
class: Drupal\simple_sitemap\Form\Form
arguments: ['@simple_sitemap.generator', '@current_user']
simple_sitemap.bundle_url_generator:
class: Drupal\simple_sitemap\BatchBundleUrlGenerator
arguments: ['@simple_sitemap.generator', '@language_manager', '@entity_type.manager', '@path.validator', '@entity.query']
simple_sitemap.batch:
class: Drupal\simple_sitemap\Batch\Batch
simple_sitemap.custom_url_generator:
class: Drupal\simple_sitemap\BatchCustomUrlGenerator
arguments: ['@simple_sitemap.generator', '@language_manager', '@entity_type.manager', '@path.validator']
simple_sitemap.batch_url_generator:
class: Drupal\simple_sitemap\Batch\BatchUrlGenerator
arguments: ['@simple_sitemap.sitemap_generator', '@language_manager', '@entity_type.manager', '@path.validator', '@entity.query']
<?php
namespace Drupal\simple_sitemap;
namespace Drupal\simple_sitemap\Batch;
use Drupal\user\Entity\User;
use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache;
use Drupal\Core\StringTranslation\StringTranslationTrait;
class Batch {
use StringTranslationTrait;
private $batch;
private $batchInfo;
const BATCH_INIT_MESSAGE = 'Initializing batch...';
const BATCH_ERROR_MESSAGE = 'An error has occurred. This may result in an incomplete XML sitemap.';
const BATCH_PROGRESS_MESSAGE = 'Processing @current out of @total link types.';
/**
* Batch constructor.
*/
public function __construct() {
$this->batch = [
'title' => $this->t('Generating XML sitemap'),
......@@ -29,6 +29,9 @@ class Batch {
];
}
/**
* @param $batch_info
*/
public function setBatchInfo($batch_info) {
$this->batchInfo = $batch_info;
}
......@@ -81,26 +84,6 @@ class Batch {
];
}
/**
* Callback function called by the batch API when all operations are finished.
*
* @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
*/
public static function finishGeneration($success, $results, $operations) {
if ($success) {
$remove_sitemap = empty($results['chunk_count']);
if (!empty($results['generate']) || $remove_sitemap) {
\Drupal::service('simple_sitemap.sitemap_generator')->generateSitemap($results['generate'], $remove_sitemap);
}
Cache::invalidateTags(['simple_sitemap']);
drupal_set_message(t("The <a href='@url' target='_blank'>XML sitemap</a> has been regenerated for all languages.",
['@url' => $GLOBALS['base_url'] . '/sitemap.xml']));
}
else {
//todo: register error
}
}
/**
* Batch callback function which generates urls to entity paths.
*
......@@ -109,7 +92,7 @@ class Batch {
* @param array &$context
*/
public static function generateBundleUrls($entity_info, $batch_info, &$context) {
\Drupal::service('simple_sitemap.bundle_url_generator')->generateBundleUrls($entity_info, $batch_info, $context);
\Drupal::service('simple_sitemap.batch_url_generator')->generateBundleUrls($entity_info, $batch_info, $context);
}
/**
......@@ -120,6 +103,17 @@ class Batch {
* @param array &$context
*/
public static function generateCustomUrls($custom_paths, $batch_info, &$context) {
\Drupal::service('simple_sitemap.custom_url_generator')->generateCustomUrls($custom_paths, $batch_info, $context);
\Drupal::service('simple_sitemap.batch_url_generator')->generateCustomUrls($custom_paths, $batch_info, $context);
}
/**
* Callback function called by the batch API when all operations are finished.
*
* @param $success
* @param $results
* @param $operations
*/
public static function finishGeneration($success, $results, $operations) {
\Drupal::service('simple_sitemap.batch_url_generator')->finishGeneration($success, $results, $operations);
}
}
<?php
namespace Drupal\simple_sitemap\Batch;
use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Class BatchUrlGenerator
* @package Drupal\simple_sitemap\Batch
*/
class BatchUrlGenerator {
use StringTranslationTrait;
const ANONYMOUS_USER_ID = 0;
const PATH_DOES_NOT_EXIST_OR_NO_ACCESS = "The path @path has been omitted from the XML sitemap as it either does not exist, or it is not accessible to anonymous users.";
protected $sitemapGenerator;
protected $languages;
protected $entityTypeManager;
protected $pathValidator;
protected $entityQuery;
protected $anonUser;
public function __construct(
$sitemap_generator,
$language_manager,
$entity_type_manager,
$path_validator,
$entity_query
) {
$this->sitemapGenerator = $sitemap_generator; //todo using only one method, maybe make method static instead?
$this->languages = $language_manager->getLanguages();
$this->entityTypeManager = $entity_type_manager;
$this->pathValidator = $path_validator;
$this->entityQuery = $entity_query;
$this->anonUser = $this->entityTypeManager->getStorage('user')->load(self::ANONYMOUS_USER_ID);
}
/**
* @param $batch_info
* @return bool
*/
protected function isBatch($batch_info) {
return $batch_info['from'] != 'nobatch';
}
/**
* @param $context
* @return bool
*/
protected function needsInitialization($context) {
return empty($context['sandbox']);
}
/**
* @param $path
* @param $context
* @return bool
*/
protected function pathProcessed($path, &$context) {
$path_pool = isset($context['results']['processed_paths']) ? $context['results']['processed_paths'] : [];
if (in_array($path, $path_pool)) {
return TRUE;
}
$context['results']['processed_paths'][] = $path;
return FALSE;
}
/**
* @param $batch_info
* @param $max
* @param $context
*/
protected function initializeBatch($batch_info, $max, &$context) {
$context['results']['generate'] = !empty($context['results']['generate']) ? $context['results']['generate'] : [];
if ( $this->isBatch($batch_info)) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_id'] = 0;
$context['sandbox']['max'] = $max;
$context['results']['processed_paths'] = !empty($context['results']['processed_paths'])
? $context['results']['processed_paths'] : [];
}
}
/**
* @param $id
* @param $context
*/
protected function setCurrentId($id, &$context) {
$context['sandbox']['progress']++;
$context['sandbox']['current_id'] = $id;
}
/**
* @param $context
*/
protected function setProgressInfo(&$context) {
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
// Providing progress info to the batch API.
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
// Adding processing message after finishing every batch segment.
end($context['results']['generate']);
$last_key = key($context['results']['generate']);
if (!empty($context['results']['generate'][$last_key]['path'])) {
$context['message'] = t("Processing path @current out of @max: @path", [
'@current' => $context['sandbox']['progress'],
'@max' => $context['sandbox']['max'],
'@path' => HTML::escape($context['results']['generate'][$last_key]['path']),
]);
}
}
}
/**
* @param $context
* @param $batch_info
*/
protected function processSegment(&$context, $batch_info) {
if (!empty($batch_info['max_links']) && count($context['results']['generate']) >= $batch_info['max_links']) {
$chunks = array_chunk($context['results']['generate'], $batch_info['max_links']);
foreach ($chunks as $i => $chunk_links) {
if (count($chunk_links) == $batch_info['max_links']) {
$remove_sitemap = empty($context['results']['chunk_count']);
$this->sitemapGenerator->generateSitemap($chunk_links, $remove_sitemap);
$context['results']['chunk_count'] = !isset($context['results']['chunk_count'])
? 1 : $context['results']['chunk_count'] + 1;
$context['results']['generate'] = array_slice($context['results']['generate'], count($chunk_links));
}
}
}
}
/**
* Logs and displays an error.
*
* @param $message
* Untranslated message.
* @param array $substitutions (optional)
* Substitutions (placeholder => substitution) which will replace placeholders
* with strings.
* @param string $type (optional)
* Message type (status/warning/error).
*/
protected function registerError($message, $substitutions = [], $type = 'error') {
$message = strtr(t($message), $substitutions);
\Drupal::logger('simple_sitemap')->notice($message); //todo DI
drupal_set_message($message, $type);
}
/**
* Batch callback function which generates urls to entity paths.
*
* @param array $entity_info
* @param array $batch_info
* @param array &$context
*
* @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
*/
public function generateBundleUrls($entity_info, $batch_info, &$context) {
$query = $this->entityQuery->get($entity_info['entity_type_name']);//todo
if (!empty($entity_info['keys']['id']))
$query->sort($entity_info['keys']['id'], 'ASC');
if (!empty($entity_info['keys']['bundle']))
$query->condition($entity_info['keys']['bundle'], $entity_info['bundle_name']);
if (!empty($entity_info['keys']['status']))
$query->condition($entity_info['keys']['status'], 1);
// Initialize batch if not done yet.
if ($this->needsInitialization($context)) {
$count_query = clone $query;
$this->initializeBatch($batch_info, $count_query->count()->execute(), $context);
}
// Creating a query limited to n=batch_process_limit entries.
if ($this->isBatch($batch_info)) {
$query->range($context['sandbox']['progress'], $batch_info['batch_process_limit']);
}
$results = $query->execute();
if (!empty($results)) {
$entities = $this->entityTypeManager->getStorage($entity_info['entity_type_name'])->loadMultiple($results);
foreach ($entities as $entity_id => $entity) {
if ($this->isBatch($batch_info)) {
$this->setCurrentId($entity_id, $context);
}
// Overriding entity settings if it has been overridden on entity edit page...
if (isset($batch_info['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['index'])) {
// Skipping entity if it has been excluded on entity edit page.
if (!$batch_info['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['index']) {
continue;
}
// Otherwise overriding priority settings for this entity.
$priority = $batch_info['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['priority'];
}
switch ($entity_info['entity_type_name']) {
case 'menu_link_content': // Loading url object for menu links.
if (!$entity->isEnabled())
continue;
$url_object = $entity->getUrlObject();
break;
default: // Loading url object for other entities.
$url_object = $entity->toUrl(); //todo: file entity type does not have a canonical url and breaks generation, hopefully fixed in https://www.drupal.org/node/2402533
}
// Do not include external paths.
if (!$url_object->isRouted())
continue;
// Do not include paths inaccessible to anonymous users.
if (!$url_object->access($this->anonUser))
continue;
// Do not include paths that have been already indexed.
$path = $url_object->getInternalPath();
if ($batch_info['remove_duplicates'] && $this->pathProcessed($path, $context))
continue;
$url_object->setOption('absolute', TRUE);
$path_data = [
'path' => $path,
'entity_info' => ['entity_type' => $entity_info['entity_type_name'], 'id' => $entity_id],
'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
'priority' => isset($priority) ? $priority : (isset($entity_info['bundle_settings']['priority']) ? $entity_info['bundle_settings']['priority'] : NULL),
];
$priority = NULL;
$alternate_urls = [];
foreach ($this->languages as $language) {
$langcode = $language->getId();
if (!$batch_info['skip_untranslated'] || $language->isDefault() || $entity->hasTranslation($langcode)) {
$url_object->setOption('language', $language);
$alternate_urls[$langcode] = $url_object->toString();
}
}
foreach($alternate_urls as $langcode => $url) {
$context['results']['generate'][] = $path_data + ['langcode' => $langcode, 'url' => $url, 'alternate_urls' => $alternate_urls];
}
}
}
if ($this->isBatch($batch_info)) {
$this->setProgressInfo($context);
}
$this->processSegment($context, $batch_info);
}
/**
* Batch function which generates urls to custom paths.
*
* @param array $custom_paths
* @param array $batch_info
* @param array &$context
*
* @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
*/
public function generateCustomUrls($custom_paths, $batch_info, &$context) {
// Initialize batch if not done yet.
if ($this->needsInitialization($context)) {
$this->initializeBatch($batch_info, count($custom_paths), $context);
}
foreach($custom_paths as $i => $custom_path) {
if ($this->isBatch($batch_info)) {
$this->setCurrentId($i, $context);
}
if (!$this->pathValidator->isValid($custom_path['path'])) { //todo: Change to different function, as this also checks if current user has access. The user however varies depending if process was started from the web interface or via cron/drush. Use getUrlIfValidWithoutAccessCheck()?
$this->registerError(self::PATH_DOES_NOT_EXIST_OR_NO_ACCESS, ['@path' => $custom_path['path']], 'warning');
continue;
}
$url_object = Url::fromUserInput($custom_path['path'], ['absolute' => TRUE]);
if (!$url_object->access($this->anonUser))
continue;
$path = $url_object->getInternalPath();
if ($batch_info['remove_duplicates'] && $this->pathProcessed($path, $context))
continue;
// Load entity object if this is an entity route.
$route_parameters = $url_object->getRouteParameters();
$entity = !empty($route_parameters)
? $this->entityTypeManager->getStorage(key($route_parameters))->load($route_parameters[key($route_parameters)])
: NULL;
$path_data = [
'path' => $path,
'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
'priority' => isset($custom_path['priority']) ? $custom_path['priority'] : NULL,
];
if (!is_null($entity)) {
$path_data['entity_info'] = ['entity_type' => $entity->getEntityTypeId(), 'id' => $entity->id()];
}
$alternate_urls = [];
foreach ($this->languages as $language) {
$langcode = $language->getId();
if (!$batch_info['skip_untranslated'] || is_null($entity) || $entity->hasTranslation($langcode) || $language->isDefault()) {
$url_object->setOption('language', $language);
$alternate_urls[$langcode] = $url_object->toString();
}
}
foreach($alternate_urls as $langcode => $url) {
$context['results']['generate'][] = $path_data + ['langcode' => $langcode, 'url' => $url, 'alternate_urls' => $alternate_urls];
}
}
if ($this->isBatch($batch_info)) {
$this->setProgressInfo($context);
}
$this->processSegment($context, $batch_info);
}
/**
* Callback function called by the batch API when all operations are finished.
*
* @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
*/
public function finishGeneration($success, $results, $operations) {
if ($success) {
$remove_sitemap = empty($results['chunk_count']);
if (!empty($results['generate']) || $remove_sitemap) {
$this->sitemapGenerator->generateSitemap($results['generate'], $remove_sitemap);
}
Cache::invalidateTags(['simple_sitemap']);
drupal_set_message($this->t("The <a href='@url' target='_blank'>XML sitemap</a> has been regenerated for all languages.",
['@url' => $GLOBALS['base_url'] . '/sitemap.xml']));
}
else {
//todo: register error
}
}
}
<?php
namespace Drupal\simple_sitemap;
namespace Drupal\simple_sitemap\Form;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Form class.
* Class Form
* @package Drupal\simple_sitemap\Form
*/
class Form {
use StringTranslationTrait;
......@@ -15,6 +16,7 @@ class Form {
const PRIORITY_DIVIDER = 10;
private $generator;
private $currentUser;
private $formState;
public $alteringForm = TRUE;
......@@ -38,11 +40,19 @@ class Form {
/**
* Form constructor.
*
* @param $generator
* @param $current_user
*/
public function __construct($generator) {
public function __construct($generator, $current_user) {
$this->generator = $generator;
$this->currentUser = $current_user;
}
/**
* @param $form_state
* @return $this
*/
public function processForm($form_state) {
$this->formState = $form_state;
if (!is_null($this->formState)) {
......@@ -52,21 +62,37 @@ class Form {
return $this;
}
/**
* @param $entity_category
* @return $this
*/
public function setEntityCategory($entity_category) {
$this->entityCategory = $entity_category;
return $this;
}
/**
* @param $entity_type_id
* @return $this
*/
public function setEntityTypeId($entity_type_id) {
$this->entityTypeId = $entity_type_id;
return $this;
}
/**
* @param $bundle_name
* @return $this
*/
public function setBundleName($bundle_name) {
$this->bundleName = $bundle_name;
return $this;
}
/**
* @param $instance_id
* @return $this
*/
public function setInstanceId($instance_id) {
$this->instanceId = $instance_id;
return $this;
......@@ -75,7 +101,7 @@ class Form {
private function assertAlteringForm() {
// Do not alter the form if user lacks certain permissions.
if (!\Drupal::currentUser()->hasPermission('administer sitemap settings'))
if (!$this->currentUser->hasPermission('administer sitemap settings'))
$this->alteringForm = FALSE;
// Do not alter the form if it is irrelevant to sitemap generation.
......@@ -93,6 +119,9 @@ class Form {
$this->alteringForm = FALSE;
}
/**
* @param $form_fragment
*/
public function displayRegenerateNow(&$form_fragment) {
$form_fragment['simple_sitemap_regenerate_now'] = [
'#type' => 'checkbox',
......@@ -104,7 +133,12 @@ class Form {
$form_fragment['simple_sitemap_regenerate_now']['#description'] .= '</br>' . $this->t('Otherwise the sitemap will be regenerated on the next cron run.');
}
}
/**
* @param $form_fragment
* @param bool $multiple
* @return $this
*/
public function displayEntitySettings(&$form_fragment, $multiple = FALSE) {
$prefix = $multiple ? $this->entityTypeId . '_' : '';
......@@ -142,7 +176,7 @@ class Form {
'#title' => $this->t('Priority'),
'#description' => $priority_description,
'#default_value' => $priority,
'#options' => self::getPrioritySelectValues(),
'#options' => $this->getPrioritySelectValues(),
];
if ($this->entityCategory == 'instance' && isset($bundle_settings['priority'])) {
$form_fragment[$prefix . 'simple_sitemap_priority']['#options'][(string)$bundle_settings['priority']] .= ' (' . $this->t('Default') . ')';
......@@ -230,10 +264,12 @@ class Form {
* Checks if simple_sitemap values have been changed after submitting the form.
* To be used in an entity form submit.
*
* @param $form
* @param $values
* @return bool
* TRUE if simple_sitemap form values have been altered by the user.
*/
public static function valuesChanged($form, $values) {
public function valuesChanged($form, $values) { //todo make non-static
foreach (self::$valuesToCheck as $field_name) {
if (isset($values[$field_name]) && $values[$field_name] != $form['simple_sitemap'][$field_name]['#default_value']) {
return TRUE;
......@@ -245,21 +281,29 @@ class Form {
/**
* Gets the values needed to display the priority dropdown setting.
*
* @return array $options
* @return array
*/
public static function getPrioritySelectValues() {
public function getPrioritySelectValues() {
$options = [];
foreach(range(0, self::PRIORITY_HIGHEST) as $value) {
$value = self::formatPriority($value / self::PRIORITY_DIVIDER);
$value = $this->formatPriority($value / self::PRIORITY_DIVIDER);
$options[$value] = $value;
}
return $options;
}
public static function formatPriority($priority) {
/**
* @param $priority
* @return string
*/
public function formatPriority($priority) {
return number_format((float)$priority, 1, '.', '');
}
/**
* @param $priority
* @return bool
*/
public static function isValidPriority($priority) {
return !is_numeric($priority) || $priority < 0 || $priority > 1 ? FALSE : TRUE;
}
......
......@@ -3,7 +3,6 @@
namespace Drupal\simple_sitemap\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\simple_sitemap\Form;
/**
* Class SimplesitemapCustomLinksForm
......@@ -26,7 +25,7 @@ class SimplesitemapCustomLinksForm extends SimplesitemapFormBase {
$setting_string = '';
foreach ($this->generator->getConfig('custom') as $custom_link) {
$setting_string .= isset($custom_link['priority'])
? $custom_link['path'] . ' ' . Form::formatPriority($custom_link['priority'])
? $custom_link['path'] . ' ' . $this->form->formatPriority($custom_link['priority'])
: $custom_link['path'];
$setting_string .= "\r\n";
}
......
......@@ -3,6 +3,7 @@
namespace Drupal\simple_sitemap;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\simple_sitemap\Form\Form;
/**
* Class Simplesitemap
......@@ -10,13 +11,15 @@ use Drupal\Core\Entity\ContentEntityTypeInterface;
*/
class Simplesitemap {
private $sitemapGenerator;
private $configFactory;
private $config;
private $db;
private $entityTypeManager;
private $pathValidator;
private static $allowed_link_settings = [
'entity' => ['index', 'priority'],
'custom' => ['priority']];
'custom' => ['priority']
];
/**
* Simplesitemap constructor.
......@@ -26,14 +29,17 @@ class Simplesitemap {
* @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
*/
public function __construct(
$sitemapGenerator,
\Drupal\Core\Config\ConfigFactoryInterface $configFactoryInterface,
$database,
\Drupal\Core\Entity\EntityTypeManager $entityTypeManager) {
\Drupal\Core\Entity\EntityTypeManager $entityTypeManager,
$pathValidator
) {
$this->sitemapGenerator = $sitemapGenerator;
$this->configFactory = $configFactoryInterface;
$this->db = $database;
$this->entityTypeManager = $entityTypeManager;
$this->config = $this->configFactory->get('simple_sitemap.settings');
$this->pathValidator = $pathValidator;
}
/**
......@@ -45,7 +51,7 @@ class Simplesitemap {
* The requested configuration.
*/
public function getConfig($key) {
return $this->config->get($key);
return $this->configFactory->get('simple_sitemap.settings')->get($key);
}
private function fetchSitemapChunks() {
......@@ -67,8 +73,6 @@ class Simplesitemap {
public function saveConfig($key, $value) {
$this->configFactory->getEditable('simple_sitemap.settings')
->set($key, $value)->save();
// Refresh config object after making changes.
$this->config = $this->configFactory->get('simple_sitemap.settings');