arch-8.x-1.x-dev/modules/product/arch_product.module
modules/product/arch_product.module
<?php
/**
* @file
* Product module.
*/
use Drupal\arch_product\Entity\Product;
use Drupal\arch_product\Entity\ProductAvailabilityInterface;
use Drupal\arch_product\Entity\ProductInterface;
use Drupal\arch_product\Entity\ProductType;
use Drupal\Component\Utility\Environment;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\language\ConfigurableLanguageInterface;
/**
* Implements hook_help().
*/
function arch_product_help($route_name, RouteMatchInterface $route_match) {
// Remind site administrators about the {arch_product_access} table being
// flagged for rebuild. We don't need to issue the message on the confirm
// form, or while the rebuild is being processed.
if (
$route_name != 'arch.product.configure_rebuild_confirm'
&& $route_name != 'system.batch_page.normal'
&& $route_name != 'help.page.arch'
&& $route_name != 'help.main'
&& \Drupal::currentUser()->hasPermission('access administration pages')
&& arch_product__product_access_needs_rebuild()
) {
if ($route_name == 'system.status') {
$message = t('The product access permissions need to be rebuilt.');
}
else {
$message = t('The product access permissions need to be rebuilt. <a href=":product_access_rebuild">Rebuild permissions</a>.', [
':product_access_rebuild' => Url::fromRoute('arch.product.configure_rebuild_confirm')->toString(),
]);
}
\Drupal::service('messenger')->addMessage($message, 'error');
}
switch ($route_name) {
case 'help.page.arch':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Arch module manages the creation, editing, deletion, settings, and display of the main site products. Products managed by the Arch module are typically displayed as list items or product page on your site, and include a title, some meta-data (author, creation time, product type, etc.), and optional fields containing text or other data (fields are managed by the <a href=":field">Field module</a>).', [
':field' => Url::fromRoute('help.page', ['name' => 'field']),
]) . '</p>';
return $output;
case 'product_type.add':
return '<p>' . t('Individual product types can have different fields, behaviors, and permissions assigned to them.') . '</p>';
case 'entity.entity_form_display.product.default':
case 'entity.entity_form_display.product.form_mode':
$type = $route_match->getParameter('product_type');
return '<p>' . t('Product items can be edited using different form modes. Here, you can define which fields are shown and hidden when %type product is edited in each form mode, and define how the field form widgets are displayed in each form mode.', ['%type' => $type->label()]) . '</p>';
case 'entity.entity_view_display.product.default':
case 'entity.entity_view_display.product.view_mode':
$type = $route_match->getParameter('product_type');
return '<p>' . t('Product items can be displayed using different view modes: Teaser, Full content, Print, RSS, etc. <em>Teaser</em> is a short format that is typically used in lists of multiple product items. <em>Full content</em> is typically used when the product is displayed on its own page.') . '</p>' .
'<p>' . t('Here, you can define which fields are shown and hidden when %type product is displayed in each view mode, and define how the fields are displayed in each view mode.', ['%type' => $type->label()]) . '</p>';
case 'entity.product.version_history':
return '<p>' . t('Revisions allow you to track differences between multiple versions of your product, and revert to older versions.') . '</p>';
case 'entity.product.edit_form':
$product = $route_match->getParameter('product');
$type = ProductType::load($product->getType());
$help = $type->getHelp();
return (!empty($help) ? Xss::filterAdmin($help) : '');
case 'product.add':
$type = $route_match->getParameter('product_type');
$help = $type->getHelp();
return (!empty($help) ? Xss::filterAdmin($help) : '');
}
}
/**
* Implements hook_theme().
*/
function arch_product_theme() {
return [
'product' => [
'render element' => 'elements',
],
'product_add_list' => [
'variables' => ['content' => NULL],
],
'product_edit_form' => [
'render element' => 'form',
],
'field__product__title' => [
'base hook' => 'field',
],
'field__product__uid' => [
'base hook' => 'field',
],
'field__product__created' => [
'base hook' => 'field',
],
];
}
/**
* Implements hook_entity_view_display_alter().
*/
function arch_product_entity_view_display_alter(EntityViewDisplayInterface $display, $context) {
if ($context['entity_type'] == 'product') {
// Hide field labels in search index.
if ($context['view_mode'] == 'search_index') {
foreach ($display->getComponents() as $name => $options) {
if (isset($options['label'])) {
$options['label'] = 'hidden';
$display->setComponent($name, $options);
}
}
}
}
}
/**
* Returns a list of available product type names.
*
* This list can include types that are queued for addition or deletion.
*
* @return string[]
* An array of product type labels, keyed by the product type name.
*/
function arch_product_type_get_names() {
return array_map(function ($bundle_info) {
return $bundle_info['label'];
}, \Drupal::service('entity_type.bundle.info')->getBundleInfo('product'));
}
/**
* Returns the product type label for the passed product.
*
* @param \Drupal\arch_product\Entity\ProductInterface $product
* A product entity to return the product type's label for.
*
* @return string|false
* The product type label or FALSE if the product type is not found.
*
* @todo Add this as generic helper method for config entities representing
* entity bundles.
*/
function arch_product_get_type_label(ProductInterface $product) {
$type = ProductType::load($product->bundle());
return $type ? $type->label() : FALSE;
}
/**
* Implements hook_entity_extra_field_info().
*/
function arch_product_entity_extra_field_info() {
$extra = [];
foreach (ProductType::loadMultiple() as $bundle) {
$extra['product'][$bundle->id()]['display']['links'] = [
'label' => t('Links'),
'weight' => 100,
'visible' => TRUE,
];
}
return $extra;
}
/**
* Updates all products of one type to be of another type.
*
* @param string $old_id
* The current product type of the products.
* @param string $new_id
* The new product type of the products.
*
* @return int
* The number of products whose product type field was modified.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
function arch_product_type_update_products($old_id, $new_id) {
return \Drupal::entityTypeManager()->getStorage('product')->updateType($old_id, $new_id);
}
/**
* Checks if the current page is the full page view of the passed-in product.
*
* @param Drupal\arch_product\Entity\ProductInterface $product
* A product entity.
*
* @return int|false
* The ID of the product if this is a full page view, otherwise FALSE.
*/
function arch_product_is_page(ProductInterface $product) {
$route_match = \Drupal::routeMatch();
if ($route_match->getRouteName() == 'entity.product.canonical') {
$page_product = $route_match->getParameter('product');
}
return (!empty($page_product) ? $page_product->id() == $product->id() : FALSE);
}
/**
* Prepares variables for list of available product type templates.
*
* Default template: product-add-list.html.twig.
*
* @param array $variables
* An associative array containing:
* - content: An array of product types.
*
* @see product_add_page()
*/
function template_preprocess_product_add_list(array &$variables) {
$variables['types'] = [];
if (!empty($variables['content'])) {
foreach ($variables['content'] as $type) {
/** @var \Drupal\arch_product\Entity\ProductTypeInterface $type */
$link = Link::fromTextAndUrl(
$type->label(),
new Url('product.add', ['product_type' => $type->id()])
);
$variables['types'][$type->id()] = [
'type' => $type->id(),
'add_link' => $link->toRenderable(),
'description' => [
'#markup' => $type->getDescription(),
],
];
}
}
}
/**
* Implements hook_preprocess_HOOK() for HTML document templates.
*/
function arch_product_preprocess_html(&$variables) {
// If on an individual product page, add the product type to description
// classes.
if ($product = \Drupal::routeMatch()->getParameter('product')) {
if (is_scalar($product)) {
$product = \Drupal::entityTypeManager()->getStorage('product')->load($product);
}
if ($product instanceof ProductInterface) {
$variables['product_type'] = $product->getType();
}
}
}
/**
* Implements hook_preprocess_HOOK() for block templates.
*/
function arch_product_preprocess_block(&$variables) {
if ($variables['configuration']['provider'] == 'arch') {
switch ($variables['elements']['#plugin_id']) {
case 'product_syndicate_block':
$variables['attributes']['role'] = 'complementary';
break;
}
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function arch_product_theme_suggestions_product(array $variables) {
$suggestions = [];
/** @var \Drupal\arch_product\Entity\ProductInterface $product */
$product = $variables['elements']['#product'];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'product__' . $sanitized_view_mode;
$suggestions[] = 'product__' . $product->bundle();
$suggestions[] = 'product__' . $product->bundle() . '__' . $sanitized_view_mode;
$suggestions[] = 'product__' . $product->id();
$suggestions[] = 'product__' . $product->id() . '__' . $sanitized_view_mode;
return $suggestions;
}
/**
* Prepares variables for product templates.
*
* Default template: product.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An array of elements to display in view mode.
* - product: The product object.
* - view_mode: View mode; e.g., 'full', 'teaser', etc.
*/
function template_preprocess_product(array &$variables) {
$variables['view_mode'] = $variables['elements']['#view_mode'];
// Provide a distinct $teaser boolean.
$variables['teaser'] = $variables['view_mode'] == 'teaser';
$variables['product'] = $variables['elements']['#product'];
/** @var \Drupal\arch_product\Entity\ProductInterface $product */
$product = $variables['product'];
$variables['date'] = \Drupal::service('renderer')->render($variables['elements']['created']);
unset($variables['elements']['created']);
$variables['author_name'] = \Drupal::service('renderer')->render($variables['elements']['uid']);
unset($variables['elements']['uid']);
$variables['url'] = $product->toUrl('canonical', [
'language' => $product->language(),
]);
$variables['label'] = $variables['elements']['title'];
unset($variables['elements']['title']);
// The 'page' variable is set to TRUE in two occasions:
// - The view mode is 'full' and we are on the 'product.view' route.
// - The product is in preview and view mode is either 'full' or 'default'.
if (array_key_exists('#page', $variables['elements'])) {
$variables['page'] = (bool) $variables['elements']['#page'];
}
else {
$variables['page'] = (
$variables['view_mode'] == 'full'
&& arch_product_is_page($product)
|| (
isset($product->inPreview)
&& in_array($product->preview_view_mode, ['full', 'default'])
)
);
}
// Helpful $content variable for templates.
$variables += ['content' => []];
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
// Used by RDF to add attributes around the author and date submitted.
$variables['author_attributes'] = new Attribute();
// Add article ARIA role.
$variables['attributes']['role'] = 'article';
}
/**
* Implements hook_user_cancel().
*/
function arch_product_user_cancel($edit, $account, $method) {
switch ($method) {
case 'user_cancel_block_unpublish':
// Unpublish products (current revisions).
$pids = \Drupal::entityQuery('product')
->condition('uid', $account->id())
->execute();
\Drupal::moduleHandler()->loadInclude('arch_product', 'inc', 'inc/admin');
product_mass_update($pids, ['status' => 0], NULL, TRUE);
break;
case 'user_cancel_reassign':
// Anonymize all of the products for this old account.
\Drupal::moduleHandler()->loadInclude('arch_product', 'inc', 'inc/admin');
$vids = \Drupal::entityTypeManager()->getStorage('product')->userRevisionIds($account);
product_mass_update($vids, [
'uid' => 0,
'revision_uid' => 0,
], NULL, TRUE, TRUE);
break;
}
}
/**
* Implements hook_ENTITY_TYPE_predelete() for user entities.
*/
function arch_product_user_predelete($account) {
// Delete products (current revisions).
// @todo Introduce product_mass_delete() or make product_mass_update() more flexible.
$pids = \Drupal::entityQuery('product')
->condition('uid', $account->id())
->accessCheck(FALSE)
->execute();
/** @var \Drupal\arch_product\Entity\Storage\ProductStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('product');
$entities = $storage->loadMultiple($pids);
$storage->delete($entities);
// Delete old revisions.
$revisions = $storage->userRevisionIds($account);
foreach ($revisions as $revision) {
$storage->deleteRevision($revision);
}
}
/**
* Finds the most recently changed products that are available to the user.
*
* @param int $number
* (optional) The maximum number of products to find. Defaults to 10.
*
* @return \Drupal\arch_product\Entity\ProductInterface[]
* An array of product entities or an empty array if there are no recent
* products visible to the current user.
*/
function arch_product_get_recent($number = 10) {
$account = \Drupal::currentUser();
$query = \Drupal::entityQuery('product');
if (!$account->hasPermission('bypass product access')) {
// If the user is able to view their own unpublished products, allow them
// to see these in addition to published products. Check that they actually
// have some unpublished products to view before adding the condition.
$access_query = \Drupal::entityQuery('product')
->condition('uid', $account->id())
->condition('status', ProductInterface::NOT_PUBLISHED);
if (
$account->hasPermission('view own unpublished product')
&& ($own_unpublished = $access_query->execute())
) {
$query->orConditionGroup()
->condition('status', ProductInterface::PUBLISHED)
->condition('pid', $own_unpublished, 'IN');
}
else {
// If not, restrict the query to published products.
$query->condition('status', ProductInterface::PUBLISHED);
}
}
$pids = $query
->sort('changed', 'DESC')
->range(0, $number)
->addTag('product_access')
->execute();
$products = Product::loadMultiple($pids);
return $products ? $products : [];
}
/**
* Implements hook_page_top().
*/
function arch_product_page_top(array &$page) {
// Add 'Back to content editing' link on preview page.
$route_match = \Drupal::routeMatch();
if ($route_match->getRouteName() == 'entity.product.preview') {
$page['page_top']['product_preview'] = [
'#type' => 'container',
'#attributes' => [
'class' => [
'product-preview-container',
'container-inline',
],
],
];
$form = \Drupal::formBuilder()->getForm('\Drupal\arch_product\Form\ProductPreviewForm', $route_match->getParameter('product_preview'));
$page['page_top']['product_preview']['view_mode'] = $form;
}
}
/**
* @defgroup product_access Product access rights
* @{
* The product access system determines who can do what to which products.
*
* In determining access rights for a product,
* \Drupal\arch_product\Access\ProductAccessControlHandler first checks whether
* the user has the "bypass product access" permission. Such users have
* unrestricted access to all products. user 1 will always pass this check.
*
* Next, all implementations of hook_product_access() will be called. Each
* implementation may explicitly allow, explicitly forbid, or ignore the access
* request. If at least one module says to forbid the request, it will be
* rejected. If no modules deny the request and at least one says to allow it,
* the request will be permitted.
*
* If all modules ignore the access request, then the product_access table is
* used to determine access. All product access modules are queried using
* hook_product_grants() to assemble a list of "grant IDs" for the user. This
* list is compared against the table. If any row contains the product ID in
* question (or 0, which stands for "all products"), one of the grant IDs
* returned, and a value of TRUE for the operation in question, then access is
* granted. Note that this table is a list of grants; any matching row is
* sufficient to grant access to the product.
*
* In product listings (lists of products generated from a select query, such as
* the default home page at path 'product', an RSS feed, a recent content block,
* etc.), the process above is followed except that hook_product_access() is not
* called on each product for performance reasons and for proper functioning of
* the pager system. When adding a product listing to your module, be sure to
* use an entity query, which will add a tag of "product_access". This will
* allow modules dealing with product access to ensure only products to which
* the user has access are retrieved, through the use of hook_query_TAG_alter().
* See the @link entity_api Entity API topic @endlink for more information on
* entity queries. Tagging a query with "product_access" does not check the
* published/unpublished status of products, so the base query is responsible
* for ensuring that unpublished products are not displayed to inappropriate
* users.
*
* Note: Even a single module returning an AccessResultInterface object from
* hook_product_access() whose isForbidden() method equals TRUE will block
* access to the product. Therefore, implementers should take care to not deny
* access unless they really intend to. Unless a module wishes to actively
* forbid access it should return an AccessResultInterface object whose
* isAllowed() nor isForbidden() methods return TRUE, to allow other modules or
* the product_access table to control access.
*
* To see how to write a product access module of your own, see
* product_access_example.module.
*/
/**
* Implements hook_product_access().
*/
function arch_product_product_access(ProductInterface $product, $op, $account) {
$type = $product->bundle();
switch ($op) {
case 'create':
return AccessResult::allowedIfHasPermission($account, 'create ' . $type . ' product');
case 'update':
if ($account->hasPermission('edit any ' . $type . ' product', $account)) {
return AccessResult::allowed()->cachePerPermissions();
}
else {
return AccessResult::allowedIf($account->hasPermission('edit own ' . $type . ' product', $account) && ($account->id() == $product->getOwnerId()))->cachePerPermissions()->cachePerUser()->addCacheableDependency($product);
}
case 'delete':
if ($account->hasPermission('delete any ' . $type . ' product', $account)) {
return AccessResult::allowed()->cachePerPermissions();
}
else {
return AccessResult::allowedIf($account->hasPermission('delete own ' . $type . ' product', $account) && ($account->id() == $product->getOwnerId()))->cachePerPermissions()->cachePerUser()->addCacheableDependency($product);
}
default:
// No opinion.
return AccessResult::neutral();
}
}
/**
* Fetches an array of permission IDs granted to the given user ID.
*
* The implementation here provides only the universal "all" grant. A produc
* access module should implement hook_product_grants() to provide a grant list
* for the user.
*
* After the default grants have been loaded, we allow modules to alter the
* grants array by reference. This hook allows for complex business logic to be
* applied when integrating multiple product access modules.
*
* @param string $op
* The operation that the user is trying to perform.
* @param \Drupal\Core\Session\AccountInterface $account
* The account object for the user performing the operation.
*
* @return array
* An associative array in which the keys are realms, and the values are
* arrays of grants for those realms.
*/
function arch_product_access_grants($op, AccountInterface $account) {
// Fetch product access grants from other modules.
$grants = \Drupal::moduleHandler()->invokeAll(
'product_grants',
[$account, $op]
);
// Allow modules to alter the assigned grants.
\Drupal::moduleHandler()->alter(
'product_grants',
$grants,
$account,
$op
);
return array_merge(['all' => [0]], $grants);
}
/**
* Determines whether the user has a global viewing grant for all products.
*
* Checks to see whether any module grants global 'view' access to a user
* account; global 'view' access is encoded in the {arch_product_access} table
* as a grant with pid=0. If no product access modules are enabled, arch.module
* defines such a global 'view' access grant.
*
* This function is called when a product listing query is tagged with
* 'product_access'; when this function returns TRUE, no product access joins
* are added to the query.
*
* @param \Drupal\Core\Session\AccountInterface|null $account
* (optional) The user object for the user whose access is being checked. If
* omitted, the current user is used. Defaults to NULL.
*
* @return bool
* TRUE if 'view' access to all products is granted, FALSE otherwise.
*
* @see hook_product_grants()
* @see arch_product_query_product_access_alter()
*/
function arch_product__product_access_view_all_products($account = NULL) {
if (!$account) {
$account = \Drupal::currentUser();
}
// Statically cache results in an array keyed by $account->id().
$access = &drupal_static(__FUNCTION__);
if (isset($access[$account->id()])) {
return $access[$account->id()];
}
// If no modules implement the product access system, access is always TRUE.
if (!\Drupal::moduleHandler()->hasImplementations('product_grants')) {
$access[$account->id()] = TRUE;
}
else {
$access[$account->id()] = \Drupal::entityTypeManager()->getAccessControlHandler('product')->checkAllGrants($account);
}
return $access[$account->id()];
}
/**
* Implements hook_query_TAG_alter().
*
* This is the hook_query_alter() for queries tagged with 'product_access'. It
* adds product access checks for the user account given by the 'account'
* meta-data (or current user if not provided), for an operation given by the
* 'op' meta-data (or 'view' if not provided; other possible values are 'update'
* and 'delete').
*
* Queries tagged with 'product_access' that are not against the {product} table
* must add the base table as metadata. For example:
* @code
* $query
* ->addTag('product_access')
* ->addMetaData('base_table', 'taxonomy_index');
* @endcode
*/
function arch_product_query_product_access_alter(AlterableInterface $query) {
// Read meta-data from query, if provided.
if (!$account = $query->getMetaData('account')) {
$account = \Drupal::currentUser();
}
if (!$op = $query->getMetaData('op')) {
$op = 'view';
}
// If $account can bypass product access, or there are no product access
// modules, or the operation is 'view' and the $account has a global view
// grant (such as a view grant for product ID 0), we don't need to alter the
// query.
if ($account->hasPermission('bypass product access')) {
return;
}
if (!\Drupal::moduleHandler()->hasImplementations('product_grants')) {
return;
}
if ($op == 'view' && arch_product__product_access_view_all_products($account)) {
return;
}
$tables = $query->getTables();
$base_table = $query->getMetaData('base_table');
// If the base table is not given, default to one of the product base tables.
if (!$base_table) {
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = \Drupal::entityTypeManager()->getStorage('product')->getTableMapping();
$product_base_tables = $table_mapping->getTableNames();
foreach ($tables as $table_info) {
if (!($table_info instanceof SelectInterface)) {
$table = $table_info['table'];
// Ensure that 'arch_product' and 'arch_product_field_data' are always
// preferred over 'arch_product_revision' and
// 'arch_product_field_revision'.
if ($table == 'arch_product' || $table == 'arch_product_field_data') {
$base_table = $table;
break;
}
// If one of the product base tables are in the query, add it to the
// list of possible base tables to join against.
if (in_array($table, $product_base_tables)) {
$base_table = $table;
}
}
}
// Bail out if the base table is missing.
if (!$base_table) {
throw new Exception('Query tagged for product access but there is no product table, specify the base_table using meta data.');
}
}
// Update the query for the given storage method.
\Drupal::service('product.grant_storage')->alterQuery($query, $tables, $op, $account, $base_table);
// Bubble the 'user.product_grants:$op' cache context to the current render
// context.
$request = \Drupal::requestStack()->getCurrentRequest();
$renderer = \Drupal::service('renderer');
if ($request->isMethodCacheable() && $renderer->hasRenderContext()) {
$build = ['#cache' => ['contexts' => ['user.product_grants:' . $op]]];
$renderer->render($build);
}
}
/**
* Toggles or reads values of a flag for rebuilding the product access grants.
*
* When the flag is set, a message is displayed to users with 'access
* administration pages' permission, pointing to the 'rebuild' confirm form.
* This can be used as an alternative to direct arch_product_access_rebuild
* calls, allowing administrators to decide when they want to perform the actual
* (possibly time consuming) rebuild.
*
* When unsure if the current user is an administrator,
* arch_product_access_rebuild() should be used instead.
*
* @param bool|null $rebuild
* (optional) The boolean value to be written.
*
* @return bool|null
* The current value of the flag if no value was provided for $rebuild. If a
* value was provided for $rebuild, nothing (NULL) is returned.
*
* @see arch_product_access_rebuild()
*/
function arch_product__product_access_needs_rebuild($rebuild = NULL) {
if (!isset($rebuild)) {
return \Drupal::state()->get('product.product_access_needs_rebuild') ?: FALSE;
}
elseif ($rebuild) {
\Drupal::state()->set('product.product_access_needs_rebuild', TRUE);
}
else {
\Drupal::state()->delete('product.product_access_needs_rebuild');
}
return NULL;
}
/**
* Rebuilds the product access database.
*
* This rebuild is occasionally needed by modules that make system-wide changes
* to access levels. When the rebuild is required by an admin-triggered action
* (e.g module settings form), calling
* arch_product__product_access_needs_rebuild(TRUE) instead of
* arch_product_access_rebuild() lets the user perform their changes and
* actually rebuild only once they are done.
*
* @param bool $batch_mode
* (optional) Set to TRUE to process in 'batch' mode, spawning processing over
* several HTTP requests (thus avoiding the risk of PHP timeout if the site
* has a large number of products). hook_update_N() and any form submit
* handler are safe contexts to use the 'batch mode'. Less decidable cases
* (such as calls from hook_user(), hook_taxonomy(), etc.) might consider
* using the non-batch mode. Defaults to FALSE.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*
* @see arch_product__product_access_needs_rebuild()
*/
function arch_product_access_rebuild($batch_mode = FALSE) {
$product_storage = \Drupal::entityTypeManager()->getStorage('product');
/** @var \Drupal\arch_product\Access\ProductAccessControlHandlerInterface $access_control_handler */
$access_control_handler = \Drupal::entityTypeManager()->getAccessControlHandler('product');
$access_control_handler->deleteGrants();
// Only recalculate if the site is using a product_access module.
if (\Drupal::moduleHandler()->hasImplementations('product_grants')) {
if ($batch_mode) {
$batch = [
'title' => t('Rebuilding product access permissions'),
'operations' => [
['arch_product__product_access_rebuild_batch_operation', []],
],
'finished' => 'arch_product__product_access_rebuild_batch_finished',
];
batch_set($batch);
}
else {
// Try to allocate enough time to rebuild product grants.
Environment::setTimeLimit(240);
// Rebuild newest products first so that recent content becomes available
// quickly.
$entity_query = \Drupal::entityQuery('product');
$entity_query->sort('pid', 'DESC');
// Disable access checking since all products must be processed even if
// the user does not have access. And unless the current user has the
// bypass product access permission, no products are accessible since the
// grants have just been deleted.
$entity_query->accessCheck(FALSE);
$pids = $entity_query->execute();
foreach ($pids as $pid) {
$product_storage->resetCache([$pid]);
$product = Product::load($pid);
// To preserve database integrity, only write grants if the product
// loads successfully.
if (!empty($product)) {
$grants = $access_control_handler->acquireGrants($product);
\Drupal::service('product.grant_storage')->write($product, $grants);
}
}
}
}
else {
// Not using any product_access modules. Add the default grant.
$access_control_handler->writeDefaultGrant();
}
if (!isset($batch)) {
\Drupal::service('messenger')->addMessage(
t('Content permissions have been rebuilt.'),
'status'
);
arch_product__product_access_needs_rebuild(FALSE);
}
}
/**
* Implements callback_batch_operation().
*
* Performs batch operation for arch_product_access_rebuild().
*
* This is a multistep operation: we go through all products by packs of 20. The
* batch processing engine interrupts processing and sends progress feedback
* after 1 second execution time.
*
* @param array $context
* An array of contextual key/value information for rebuild batch process.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
function arch_product__product_access_rebuild_batch_operation(array &$context) {
$product_storage = \Drupal::entityTypeManager()->getStorage('product');
if (empty($context['sandbox'])) {
// Initiate multistep processing.
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_product'] = 0;
$context['sandbox']['max'] = \Drupal::entityQuery('product')
->count()
->accessCheck(FALSE)
->execute();
}
// Process the next 20 products.
$limit = 20;
$pids = \Drupal::entityQuery('product')
->condition('pid', $context['sandbox']['current_product'], '>')
->sort('pid', 'ASC')
// Disable access checking since all products must be processed even if the
// user does not have access. And unless the current user has the bypass
// product access permission, no products are accessible since the grants
// have just been deleted.
->accessCheck(FALSE)
->range(0, $limit)
->execute();
$product_storage->resetCache($pids);
$products = Product::loadMultiple($pids);
foreach ($products as $pid => $product) {
// To preserve database integrity, only write grants if the product
// loads successfully.
if (!empty($product)) {
/** @var \Drupal\arch_product\Access\ProductAccessControlHandlerInterface $access_control_handler */
$access_control_handler = \Drupal::entityTypeManager()->getAccessControlHandler('product');
$grants = $access_control_handler->acquireGrants($product);
\Drupal::service('product.grant_storage')->write($product, $grants);
}
$context['sandbox']['progress']++;
$context['sandbox']['current_product'] = $pid;
}
// Multistep processing : report progress.
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
}
/**
* Implements callback_batch_finished().
*
* Performs post-processing for arch_product_access_rebuild().
*
* @param bool $success
* A boolean indicating whether the re-build process has completed.
* @param array $results
* An array of results information.
* @param array $operations
* An array of function calls (not used in this function).
*/
function arch_product__product_access_rebuild_batch_finished($success, array $results, array $operations) {
if ($success) {
\Drupal::service('messenger')->addMessage(
t('The product access permissions have been rebuilt.'),
'status'
);
arch_product__product_access_needs_rebuild(FALSE);
}
else {
\Drupal::service('messenger')->addMessage(
t('The product access permissions have not been properly rebuilt.'),
'error'
);
}
}
/**
* @} End of "defgroup product_access".
*/
/**
* Implements hook_modules_installed().
*/
function arch_product_modules_installed($modules) {
// Check if any of the newly enabled modules require the arch_access table to
// be rebuilt.
arch_product__product_access_needs_rebuild(TRUE);
}
/**
* Implements hook_modules_uninstalled().
*/
function arch_product_modules_uninstalled($modules) {
// Check whether any of the disabled modules implemented
// hook_product_grants(), in which case the product access table needs to be
// rebuilt.
foreach ($modules as $module) {
// At this point, the module is already disabled, but its code is still
// loaded in memory. Module functions must no longer be called. We only
// check whether a hook implementation function exists and do not invoke it.
// Product access also needs to be rebuilt if language module is disabled to
// remove any language-specific grants.
if (
!arch_product__product_access_needs_rebuild()
&& (
\Drupal::moduleHandler()->hasImplementations('product_grants', $module)
|| $module == 'language'
)
) {
arch_product__product_access_needs_rebuild(TRUE);
}
}
// If there remains no more product_access module, rebuilding will be
// straightforward, we can do it right now.
if (
arch_product__product_access_needs_rebuild()
&& !\Drupal::moduleHandler()->hasImplementations('product_grants')
) {
arch_product_access_rebuild();
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
*/
function arch_product_configurable_language_delete(ConfigurableLanguageInterface $language) {
// On products with this language, unset the language.
\Drupal::entityTypeManager()->getStorage('product')->clearRevisionsLanguage($language);
}
/**
* Marks a product to be re-indexed by the product_search plugin.
*
* @param int $pid
* The product ID.
*/
function arch_product_reindex_product_search($pid) {
if (\Drupal::hasService('search.index')) {
$search_index = \Drupal::service('search.index');
$search_index->markForReindex('product_search', $pid);
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for comment entities.
*/
function arch_product_comment_insert($comment) {
if (\Drupal::isConfigSyncing()) {
// Do not change data while config import in progress.
return;
}
// Reindex the product when comments are added.
if ($comment->getCommentedEntityTypeId() == 'product') {
arch_product_reindex_product_search($comment->getCommentedEntityId());
}
}
/**
* Implements hook_ENTITY_TYPE_update() for comment entities.
*/
function arch_product_comment_update($comment) {
if (\Drupal::isConfigSyncing()) {
// Do not change data while config import in progress.
return;
}
// Reindex the product when comments are changed.
if ($comment->getCommentedEntityTypeId() == 'product') {
arch_product_reindex_product_search($comment->getCommentedEntityId());
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for comment entities.
*/
function arch_product_comment_delete($comment) {
// Reindex the product when comments are deleted.
if ($comment->getCommentedEntityTypeId() == 'product') {
arch_product_reindex_product_search($comment->getCommentedEntityId());
}
}
/**
* Implements hook_config_translation_info_alter().
*/
function arch_product_config_translation_info_alter(&$info) {
$info['product_type']['class'] = 'Drupal\arch_product\ConfigTranslation\ProductTypeMapper';
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*
* Changes vertical tabs to container.
*/
function arch_product_form_product_form_alter(&$form, FormStateInterface $form_state) {
$form['#theme'] = ['product_edit_form'];
$form['advanced']['#type'] = 'container';
$form['meta']['#type'] = 'container';
$form['meta']['#access'] = TRUE;
$form['meta']['changed']['#wrapper_attributes']['class'][] = 'container-inline';
$form['meta']['author']['#wrapper_attributes']['class'][] = 'container-inline';
$form['revision_information']['#type'] = 'container';
$form['revision_information']['#group'] = 'meta';
}
/**
* Implements hook_element_info_alter().
*/
function arch_product_element_info_alter(array &$type) {
// Alter the language_select element so that it will be rendered like a select
// field.
if (isset($type['product_availability_select'])) {
if (!isset($type['product_availability_select']['#process'])) {
$type['product_availability_select']['#process'] = [];
}
if (!isset($type['product_availability_select']['#theme_wrappers'])) {
$type['product_availability_select']['#theme_wrappers'] = [];
}
$type['product_availability_select']['#process'] = array_merge($type['product_availability_select']['#process'], [
'arch_product_process_product_availability_select',
['Drupal\Core\Render\Element\Select', 'processSelect'],
['Drupal\Core\Render\Element\RenderElement', 'processAjaxForm'],
]);
$type['product_availability_select']['#theme'] = 'select';
$type['product_availability_select']['#theme_wrappers'] = array_merge($type['product_availability_select']['#theme_wrappers'], ['form_element']);
$type['product_availability_select']['#multiple'] = FALSE;
}
}
/**
* Processes a product availability select list form element.
*
* @param array $element
* The form element to process.
*
* @return array
* The processed form element.
*/
function arch_product_process_product_availability_select(array $element) {
// Don't set the options if another module (translation for example) already
// set the options.
if (!isset($element['#options'])) {
$element['#options'] = [
ProductAvailabilityInterface::STATUS_AVAILABLE => t('Available', [], ['context' => 'arch_product_availability']),
ProductAvailabilityInterface::STATUS_NOT_AVAILABLE => t('Not available', [], ['context' => 'arch_product_availability']),
ProductAvailabilityInterface::STATUS_PREORDER => t('Preorder', [], ['context' => 'arch_product_availability']),
];
}
return $element;
}
