Commit d4888794 authored by Pawel G's avatar Pawel G

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

parent 62b049f1
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* Main module file containing hooks. * Main module file containing hooks.
*/ */
use Drupal\simple_sitemap\Form; use Drupal\simple_sitemap\Form\Form;
/** /**
* Implements hook_help. * Implements hook_help.
...@@ -58,15 +58,16 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) { ...@@ -58,15 +58,16 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) {
*/ */
function simple_sitemap_entity_form_submit($form, &$form_state) { function simple_sitemap_entity_form_submit($form, &$form_state) {
$f = \Drupal::service('simple_sitemap.form')->processForm($form_state);
$values = $form_state->getValues(); $values = $form_state->getValues();
// Fix for values appearing in a sub array on a commerce product entity. // Fix for values appearing in a sub array on a commerce product entity.
$values = isset($values['simple_sitemap']) ? $values['simple_sitemap'] : $values; $values = isset($values['simple_sitemap']) ? $values['simple_sitemap'] : $values;
// Only make changes in DB if sitemap settings actually changed. // 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'); $generator = \Drupal::service('simple_sitemap.generator');
$f = \Drupal::service('simple_sitemap.form')->processForm($form_state);
switch ($f->entityCategory) { switch ($f->entityCategory) {
......
services: services:
simple_sitemap.generator: simple_sitemap.generator:
class: Drupal\simple_sitemap\Simplesitemap 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: simple_sitemap.sitemap_generator:
class: Drupal\simple_sitemap\SitemapGenerator 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: simple_sitemap.form:
class: Drupal\simple_sitemap\Form class: Drupal\simple_sitemap\Form\Form
arguments: ['@simple_sitemap.generator'] arguments: ['@simple_sitemap.generator', '@current_user']
simple_sitemap.bundle_url_generator: simple_sitemap.batch:
class: Drupal\simple_sitemap\BatchBundleUrlGenerator class: Drupal\simple_sitemap\Batch\Batch
arguments: ['@simple_sitemap.generator', '@language_manager', '@entity_type.manager', '@path.validator', '@entity.query']
simple_sitemap.custom_url_generator: simple_sitemap.batch_url_generator:
class: Drupal\simple_sitemap\BatchCustomUrlGenerator class: Drupal\simple_sitemap\Batch\BatchUrlGenerator
arguments: ['@simple_sitemap.generator', '@language_manager', '@entity_type.manager', '@path.validator'] arguments: ['@simple_sitemap.sitemap_generator', '@language_manager', '@entity_type.manager', '@path.validator', '@entity.query']
<?php <?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; use Drupal\Core\StringTranslation\StringTranslationTrait;
class Batch { class Batch {
use StringTranslationTrait; use StringTranslationTrait;
private $batch; private $batch;
private $batchInfo; private $batchInfo;
const BATCH_INIT_MESSAGE = 'Initializing batch...'; const BATCH_INIT_MESSAGE = 'Initializing batch...';
const BATCH_ERROR_MESSAGE = 'An error has occurred. This may result in an incomplete XML sitemap.'; 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.'; const BATCH_PROGRESS_MESSAGE = 'Processing @current out of @total link types.';
/**
* Batch constructor.
*/
public function __construct() { public function __construct() {
$this->batch = [ $this->batch = [
'title' => $this->t('Generating XML sitemap'), 'title' => $this->t('Generating XML sitemap'),
...@@ -29,6 +29,9 @@ class Batch { ...@@ -29,6 +29,9 @@ class Batch {
]; ];
} }
/**
* @param $batch_info
*/
public function setBatchInfo($batch_info) { public function setBatchInfo($batch_info) {
$this->batchInfo = $batch_info; $this->batchInfo = $batch_info;
} }
...@@ -81,26 +84,6 @@ class Batch { ...@@ -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. * Batch callback function which generates urls to entity paths.
* *
...@@ -109,7 +92,7 @@ class Batch { ...@@ -109,7 +92,7 @@ class Batch {
* @param array &$context * @param array &$context
*/ */
public static function generateBundleUrls($entity_info, $batch_info, &$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 { ...@@ -120,6 +103,17 @@ class Batch {
* @param array &$context * @param array &$context
*/ */
public static function generateCustomUrls($custom_paths, $batch_info, &$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 <?php
namespace Drupal\simple_sitemap; namespace Drupal\simple_sitemap\Form;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
/** /**
* Form class. * Class Form
* @package Drupal\simple_sitemap\Form
*/ */
class Form { class Form {
use StringTranslationTrait; use StringTranslationTrait;
...@@ -15,6 +16,7 @@ class Form { ...@@ -15,6 +16,7 @@ class Form {
const PRIORITY_DIVIDER = 10; const PRIORITY_DIVIDER = 10;
private $generator; private $generator;
private $currentUser;
private $formState; private $formState;
public $alteringForm = TRUE; public $alteringForm = TRUE;
...@@ -38,11 +40,19 @@ class Form { ...@@ -38,11 +40,19 @@ class Form {
/** /**
* Form constructor. * Form constructor.
*
* @param $generator
* @param $current_user
*/ */
public function __construct($generator) { public function __construct($generator, $current_user) {
$this->generator = $generator; $this->generator = $generator;
$this->currentUser = $current_user;
} }
/**
* @param $form_state
* @return $this
*/
public function processForm($form_state) { public function processForm($form_state) {
$this->formState = $form_state; $this->formState = $form_state;
if (!is_null($this->formState)) { if (!is_null($this->formState)) {
...@@ -52,21 +62,37 @@ class Form { ...@@ -52,21 +62,37 @@ class Form {
return $this; return $this;
} }
/**
* @param $entity_category
* @return $this
*/
public function setEntityCategory($entity_category) { public function setEntityCategory($entity_category) {
$this->entityCategory = $entity_category; $this->entityCategory = $entity_category;
return $this; return $this;
} }
/**
* @param $entity_type_id
* @return $this
*/
public function setEntityTypeId($entity_type_id) { public function setEntityTypeId($entity_type_id) {
$this->entityTypeId = $entity_type_id; $this->entityTypeId = $entity_type_id;
return $this; return $this;
} }
/**
* @param $bundle_name
* @return $this
*/
public function setBundleName($bundle_name) {