faq-8.x-1.0-alpha1/src/Controller/FaqController.php
src/Controller/FaqController.php
<?php
namespace Drupal\faq\Controller;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGeneratorInterface;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\faq\FaqHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Controller routines for FAQ routes.
*/
class FaqController extends ControllerBase {
protected $database;
protected $config;
protected $renderer;
protected $entityTypeManager;
protected $languageManager;
protected $linkGenerator;
/**
* @param \Drupal\Core\Database\Connection $database
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* @param \Drupal\Core\Render\RendererInterface $renderer
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* @param \Drupal\Core\Utility\LinkGeneratorInterface $linkGenerator
*/
public function __construct(
Connection $database,
ConfigFactoryInterface $config,
RendererInterface $renderer,
EntityTypeManagerInterface $entityTypeManager,
LanguageManagerInterface $languageManager,
LinkGeneratorInterface $linkGenerator
) {
$this->database = $database;
$this->config = $config;
$this->renderer = $renderer;
$this->entityTypeManager = $entityTypeManager;
$this->languageManager = $languageManager;
$this->linkGenerator = $linkGenerator;
}
/**
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* @return static
*/
public static function create(ContainerInterface $container) {
return new static(
// Load the service required to construct this class.
$container->get('database'),
$container->get('config.factory'),
$container->get('renderer'),
$container->get('entity_type.manager'),
$container->get('language_manager'),
$container->get('link_generator')
);
}
/**
* Function to display the faq page.
*
* @param int $tid
* Default is 0, determines if the questions and answers on the page
* will be shown according to a category or non-categorized.
* @param string $faq_display
* Optional parameter to override default question layout setting.
* @param string $category_display
* Optional parameter to override default category layout setting.
*
* @return
* The page with FAQ questions and answers.
*
* @throws NotFoundHttpException
*/
public function faqPage($tid = 0, $faq_display = '', $category_display = '') {
$faq_settings = $this->config->get('faq.settings');
$output = $output_answers = '';
$build = array();
$build['#type'] = 'markup';
$build['#attached']['library'][] = 'faq/faq-css';
$build['#title'] = $faq_settings->get('title');
if (!$this->moduleHandler()->moduleExists('taxonomy')) {
$tid = 0;
}
$faq_display = $faq_settings->get('display');
$use_categories = $faq_settings->get('use_categories');
$category_display = $faq_settings->get('category_display');
// If taxonomy doesn't installed, do not use categories.
if (!$this->moduleHandler()->moduleExists('taxonomy')) {
$use_categories = FALSE;
}
if (($use_categories && $category_display == 'hide_qa') || $faq_display == 'hide_answer') {
$build['#attached']['library'][] = 'faq/faq-scripts';
$build['#attached']['drupalSettings']['faqSettings']['hide_qa_accordion'] = $faq_settings->get('hide_qa_accordion');
$build['#attached']['drupalSettings']['faqSettings']['category_hide_qa_accordion'] = $faq_settings->get('category_hide_qa_accordion');
}
// Non-categorized questions and answers.
if (!$use_categories || ($category_display == 'none' && empty($tid))) {
if (!empty($tid)) {
throw new NotFoundHttpException();
}
$langcode = $this->languageManager->getCurrentLanguage()->getId();
$default_sorting = $faq_settings->get('default_sorting');
$query = $this->database->select('node', 'n');
$weight_alias = $query->leftJoin('faq_weights', 'w', '%alias.nid=n.nid');
$query->leftJoin('node_field_data', 'd', 'd.nid=n.nid');
$db_or = new Condition('OR');
$db_or->condition("$weight_alias.tid", 0)->isNull("$weight_alias.tid");
$query
->fields('n', ['nid'])
->condition('n.type', 'faq')
->condition('d.langcode', $langcode)
->condition('d.status', 1)
->condition($db_or)
->addTag('node_access');
$default_weight = 0;
if ($default_sorting == 'ASC') {
$default_weight = 1000000;
}
// Works, but involves variable concatenation - safe though, since
// $default_weight is an integer.
$query->addExpression("COALESCE(w.weight, $default_weight)", 'effective_weight');
// Doesn't work in Postgres.
// $query->addExpression('COALESCE(w.weight, CAST(:default_weight as SIGNED))', 'effective_weight', array(':default_weight' => $default_weight));.
$query->orderBy('effective_weight', 'ASC')
->orderBy('d.sticky', 'DESC');
if ($default_sorting == 'ASC') {
$query->orderBy('d.created', 'ASC');
}
else {
$query->orderBy('d.created', 'DESC');
}
// Only need the nid column.
$nids = $query->execute()->fetchCol();
$data = Node::loadMultiple($nids);
foreach ($data as $key => &$node) {
$node = ($node->hasTranslation($langcode)) ? $node->getTranslation($langcode) : $node;
}
$questions_to_render = array();
$questions_to_render['#data'] = $data;
switch ($faq_display) {
case 'questions_top':
$questions_to_render['#theme'] = 'faq_questions_top';
break;
case 'hide_answer':
$questions_to_render['#theme'] = 'faq_hide_answer';
break;
case 'questions_inline':
$questions_to_render['#theme'] = 'faq_questions_inline';
break;
case 'new_page':
$questions_to_render['#theme'] = 'faq_new_page';
break;
} // End of switch.
$output = \Drupal::service('renderer')->render($questions_to_render);
}
// Categorize questions.
else {
$hide_child_terms = $faq_settings->get('hide_child_terms');
// If we're viewing a specific category/term.
if (!empty($tid)) {
if ($term = Term::load($tid)) {
$title = $faq_settings->get('title');
$build['#title'] = ($title . ($title ? ' - ' : '') . $this->t($term->getName()));
$this->_displayFaqByCategory($faq_display, $category_display, $term, 0, $output, $output_answers);
$to_render = array(
'#theme' => 'faq_page',
'#content' => new FormattableMarkup($output, []),
'#answers' => new FormattableMarkup($output_answers, []),
);
$build['#markup'] = $this->renderer->render($to_render);
return $build;
}
else {
throw new NotFoundHttpException();
}
}
$list_style = $faq_settings->get('category_listing');
$vocabularies = Vocabulary::loadMultiple();
$vocab_omit = $faq_settings->get('omit_vocabulary');
$items = array();
$vocab_items = array();
foreach ($vocabularies as $vid => $vobj) {
if (isset($vocab_omit[$vid]) && ($vocab_omit[$vid] !== 0)) {
continue;
}
if ($category_display == "new_page") {
$vocab_items = $this->_getIndentedFaqTerms($vid, 0);
$items = array_merge($items, $vocab_items);
}
// Not a new page.
else {
if ($hide_child_terms && $category_display == 'hide_qa') {
$tree = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid, 0, 1, TRUE);
}
else {
$tree = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid, 0, NULL, TRUE);
}
foreach ($tree as $term) {
switch ($category_display) {
case 'hide_qa':
case 'categories_inline':
if (FaqHelper::taxonomyTermCountNodes($term->id())) {
$this->_displayFaqByCategory($faq_display, $category_display, $term, 1, $output, $output_answers);
}
break;
}
}
}
}
if ($category_display == "new_page") {
$output = $this->_renderCategoriesToList($items, $list_style);
}
}
$faq_description = $faq_settings->get('description');
$markup = array(
'#theme' => 'faq_page',
'#content' => new FormattableMarkup($output, []),
'#answers' => new FormattableMarkup($output_answers, []),
'#description' => new FormattableMarkup($faq_description, []),
);
$build['#markup'] = $this->renderer->render($markup);
return $build;
}
/**
* Define the elements for the FAQ Settings page - order tab.
*
* @param $category
* The category id of the FAQ page to reorder.
*
* @return
* The form code, before being converted to HTML format.
*/
public function orderPage($tid = NULL) {
$faq_settings = $this->config->get('faq.settings');
$build = array();
$build['#attached']['library'][] = 'faq/faq-scripts';
$build['#attached']['drupalSettings']['faqSettings']['hide_qa_accordion'] = $faq_settings->get('hide_qa_accordion');
$build['#attached']['drupalSettings']['faqSettings']['category_hide_qa_accordion'] = $faq_settings->get('category_hide_qa_accordion');
$build['#attached']['library'][] = 'faq/faq-css';
$build['faq_order'] = $this->formBuilder()->getForm('Drupal\faq\Form\OrderForm');
return $build;
}
/**
* Renders the form for the FAQ Settings page - General tab.
*
* @return
* The form code inside the $build array.
*/
public function generalSettings() {
$build = array();
$build['faq_general_settings_form'] = $this->formBuilder()->getForm('Drupal\faq\Form\GeneralForm');
return $build;
}
/**
* Renders the form for the FAQ Settings page - Questions tab.
*
* @return
* The form code inside the $build array.
*/
public function questionsSettings() {
$faq_settings = $this->config->get('faq.settings');
$build = array();
$build['#attached']['library'][] = 'faq/faq-scripts';
$build['#attached']['drupalSettings']['faqSettings']['hide_qa_accordion'] = $faq_settings->get('hide_qa_accordion');
$build['#attached']['drupalSettings']['faqSettings']['category_hide_qa_accordion'] = $faq_settings->get('category_hide_qa_accordion');
$build['faq_questions_settings_form'] = $this->formBuilder()->getForm('Drupal\faq\Form\QuestionsForm');
return $build;
}
/**
* Renders the form for the FAQ Settings page - Categories tab.
*
* @return
* The form code inside the $build array.
*/
public function categoriesSettings() {
$faq_settings = $this->config->get('faq.settings');
$build = array();
$build['#attached']['library'][] = 'faq/faq-scripts';
$build['#attached']['drupalSettings']['faqSettings']['hide_qa_accordion'] = $faq_settings->get('hide_qa_accordion');
$build['#attached']['drupalSettings']['faqSettings']['category_hide_qa_accordion'] = $faq_settings->get('category_hide_qa_accordion');
if (!$this->moduleHandler()->moduleExists('taxonomy')) {
$this->messenger()->addError(t('Categorization of questions will not work without the "taxonomy" module being enabled.'));
}
$build['faq_categories_settings_form'] = $this->formBuilder()->getForm('Drupal\faq\Form\CategoriesForm');
return $build;
}
/* ****************************************************************
* PRIVATE HELPER FUCTIONS
* *************************************************************** */
/**
* Display FAQ questions and answers filtered by category.
*
* @param $faq_display Define the way the FAQ is being shown; can have the values:
* 'questions top',hide answers','questions inline','new page'.
* @param $category_display The layout of categories which should be used.
* @param $term The category / term to display FAQs for.
* @param $display_header Set if the header will be shown or not.
* @param &$output Reference which holds the content of the page, HTML formatted.
* @param &$output_answer Reference which holds the answers from the FAQ, when showing questions on top.
*/
private function _displayFaqByCategory($faq_display, $category_display, $term, $display_header, &$output, &$output_answers) {
$langcode = $this->languageManager->getCurrentLanguage()->getId();
$default_sorting = $this->config->get('faq.settings')->get('default_sorting');
$term_id = $term->id();
$query = $this->database->select('node', 'n');
$query->join('node_field_data', 'd', 'd.nid = n.nid');
$query->innerJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
$query->leftJoin('faq_weights', 'w', 'w.tid = ti.tid AND n.nid = w.nid');
$query->fields('n', ['nid'])
->condition('n.type', 'faq')
->condition('d.langcode', $langcode)
->condition('d.status', 1)
->condition("ti.tid", $term_id)
->addTag('node_access');
$default_weight = 0;
if ($default_sorting == 'ASC') {
$default_weight = 1000000;
}
// Works, but involves variable concatenation - safe though, since
// $default_weight is an integer.
$query->addExpression("COALESCE(w.weight, $default_weight)", 'effective_weight');
// Doesn't work in Postgres.
// $query->addExpression('COALESCE(w.weight, CAST(:default_weight as SIGNED))', 'effective_weight', array(':default_weight' => $default_weight));.
$query->orderBy('effective_weight', 'ASC')
->orderBy('d.sticky', 'DESC');
if ($default_sorting == 'ASC') {
$query->orderBy('d.created', 'ASC');
}
else {
$query->orderBy('d.created', 'DESC');
}
// We only want the first column, which is nid, so that we can load all
// related nodes.
$nids = $query->execute()->fetchCol();
$data = Node::loadMultiple($nids);
foreach ($data as $key => &$node) {
$node = ($node->hasTranslation($langcode)) ? $node->getTranslation($langcode) : $node;
}
// Handle indenting of categories.
$depth = 0;
if (!isset($term->depth)) {
$children = $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren($term->id());
$term->depth = count($children);
}
while ($depth < $term->depth) {
$display_header = 1;
$indent = '<div class="faq-category-indent">';
$output .= $indent;
$depth++;
}
// Set up the class name for hiding the q/a for a category if required.
$faq_class = "faq-qa";
if ($category_display == "hide_qa") {
$faq_class = "faq-qa-hide";
}
$output_render = $output_answers_render = array(
'#data' => $data,
'#display_header' => $display_header,
'#category_display' => $category_display,
'#term' => $term,
'#class' => $faq_class,
'#parent_term' => $term,
);
switch ($faq_display) {
case 'questions_top':
$output_render['#theme'] = 'faq_category_questions_top';
$output .= $this->renderer->render($output_render);
$output_answers_render['#theme'] = 'faq_category_questions_top_answers';
$output_answers .= $this->renderer->render($output_answers_render);
break;
case 'hide_answer':
$output_render['#theme'] = 'faq_category_hide_answer';
$output .= $this->renderer->render($output_render);
break;
case 'questions_inline':
$output_render['#theme'] = 'faq_category_questions_inline';
$output .= $this->renderer->render($output_render);
break;
case 'new_page':
$output_render['#theme'] = 'faq_category_new_page';
$output .= $this->renderer->render($output_render);
break;
}
// Handle indenting of categories.
while ($depth > 0) {
$output .= '</div>';
$depth--;
}
}
/**
* Return a structured array that consists a list of terms indented according to the term depth.
*
* @param $vid
* Vocabulary id.
* @param $tid
* Term id.
*
* @return
* Return an array of a list of terms indented according to the term depth.
*/
private function _getIndentedFaqTerms($vid, $tid) {
// If ($this->moduleHandler()->moduleExists('pathauto')) {
// pathauto does't exists in D8 yet
// }.
$faq_settings = $this->config->get('faq.settings');
$display_faq_count = $faq_settings->get('count');
$hide_child_terms = $faq_settings->get('hide_child_terms');
$items = array();
$tree = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid, $tid, 1, TRUE);
foreach ($tree as $term) {
$term_id = $term->id();
$tree_count = FaqHelper::taxonomyTermCountNodes($term_id);
if ($tree_count) {
// Get term description.
$desc = '';
$term_description = $term->getDescription();
if (!empty($term_description)) {
$desc = '<div class="faq-qa-description">';
$desc .= $term_description . "</div>";
}
$query = $this->database->select('node', 'n');
$query->join('node_field_data', 'd', 'n.nid = d.nid');
$query->innerJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
$term_node_count = $query->condition('d.status', 1)
->condition('n.type', 'faq')
->condition("ti.tid", $term_id)
->addTag('node_access')
->countQuery()
->execute()
->fetchField();
if ($term_node_count > 0) {
$path = Url::fromUserInput('/faq-page/' . $term_id);
// Pathauto is not exists in D8 yet
// if (!\Drupal::service('path.alias_manager.cached')->getPathAlias(arg(0) . '/' . $tid) && $this->moduleHandler()->moduleExists('pathauto')) {
// }.
if ($display_faq_count) {
$count = $term_node_count;
if ($hide_child_terms) {
$count = $tree_count;
}
$cur_item = $this->linkGenerator->generate($this->t($term->getName()), $path) . " ($count) " . $desc;
}
else {
$cur_item = $this->linkGenerator->generate($this->t($term->getName()), $path) . $desc;
}
}
else {
$cur_item = $this->t($term->getName()) . $desc;
}
if (!empty($term_image)) {
$cur_item .= '<div class="clear-block"></div>';
}
$term_items = array();
if (!$hide_child_terms) {
$term_items = $this->_getIndentedFaqTerms($vid, $term_id);
}
$items[] = array(
"item" => $cur_item,
"children" => $term_items,
);
}
}
return $items;
}
/**
* Renders the output of getIntendedFaqTerms to HTML list.
*
* @param array $items
* The structured array made by getIntendedTerms function
* @param string $list_style
* List style type: ul or ol.
*
* @return string
* HTML formatted output.
*/
private function _renderCategoriesToList($items, $list_style) {
$list = array();
foreach ($items as $item) {
$pre = '';
if (!empty($item['children'])) {
$pre = $this->_renderCategoriesToList($item['children'], $list_style);
}
$list[] = new FormattableMarkup($item['item'] . $pre, []);
}
$render = array(
'#theme' => 'item_list',
'#items' => $list,
'#list_style' => $list_style,
);
return $this->renderer->render($render);
}
}
