first commit
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
/**
|
||||
* @file classes/issue/Collector.php
|
||||
*
|
||||
* Copyright (c) 2014-2023 Simon Fraser University
|
||||
* Copyright (c) 2000-2023 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Collector
|
||||
*
|
||||
* @brief A helper class to configure a query Builder to get a collection of issues
|
||||
*/
|
||||
|
||||
namespace APP\issue;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use Exception;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use InvalidArgumentException;
|
||||
use PKP\core\interfaces\CollectorInterface;
|
||||
use PKP\core\PKPApplication;
|
||||
use PKP\plugins\Hook;
|
||||
|
||||
class Collector implements CollectorInterface
|
||||
{
|
||||
public const ORDERBY_DATE_PUBLISHED = 'datePublished';
|
||||
public const ORDERBY_LAST_MODIFIED = 'lastModified';
|
||||
public const ORDERBY_SEQUENCE = 'seq';
|
||||
public const ORDERBY_PUBLISHED_ISSUES = 'publishedIssues';
|
||||
public const ORDERBY_UNPUBLISHED_ISSUES = 'unpublishedIssues';
|
||||
public const ORDERBY_SHELF = 'shelf';
|
||||
public const ORDER_DIR_ASC = 'ASC';
|
||||
public const ORDER_DIR_DESC = 'DESC';
|
||||
private const ORDER_CURRENT_ISSUE = 'currentIssue';
|
||||
|
||||
public DAO $dao;
|
||||
|
||||
public ?int $count = null;
|
||||
|
||||
public ?int $offset = null;
|
||||
|
||||
/** @var array|null Context ID or PKPApplication::CONTEXT_ID_ALL to get from all contexts */
|
||||
public ?array $contextIds = null;
|
||||
|
||||
/** @var array|null List of issue IDs to include */
|
||||
public ?array $issueIds = null;
|
||||
|
||||
/** @var array|null order and direction pairing for queries */
|
||||
public ?array $resultOrderings = null;
|
||||
|
||||
/** @var bool|null return published issues */
|
||||
public ?bool $isPublished = null;
|
||||
|
||||
/** @var array|null return issues in volume(s) */
|
||||
public ?array $volumes = null;
|
||||
|
||||
/** @var array|null return issues with number(s) */
|
||||
public ?array $numbers = null;
|
||||
|
||||
/** @var array|null return issues with year(s) */
|
||||
public ?array $years = null;
|
||||
|
||||
/** @var array|null return issues that match a title */
|
||||
public ?array $titles = null;
|
||||
|
||||
public ?array $doiStatuses = null;
|
||||
|
||||
public ?bool $hasDois = null;
|
||||
|
||||
/** @var array Which DOI types should be considered when checking if a submission has DOIs set */
|
||||
public array $enabledDoiTypes = [];
|
||||
|
||||
/** @var string|null Returns Issue by URL path */
|
||||
public ?string $urlPath = null;
|
||||
|
||||
/** @var string|null return issues which match words from this search phrase */
|
||||
public ?string $searchPhrase = null;
|
||||
|
||||
public function __construct(DAO $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/** @copydoc DAO::getCount() */
|
||||
public function getCount(): int
|
||||
{
|
||||
return $this->dao->getCount($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc DAO::getIds()
|
||||
*
|
||||
* @return Collection<int,int>
|
||||
*/
|
||||
public function getIds(): Collection
|
||||
{
|
||||
return $this->dao->getIds($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc DAO::getMany()
|
||||
*
|
||||
* @return LazyCollection<int,Issue>
|
||||
*/
|
||||
public function getMany(): LazyCollection
|
||||
{
|
||||
return $this->dao->getMany($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context issues filter
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByContextIds(?array $contextIds): static
|
||||
{
|
||||
$this->contextIds = $contextIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set issue ID filter
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByIssueIds(?array $issueIds): static
|
||||
{
|
||||
$this->issueIds = $issueIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set result order and direction based on an ORDERBY_* constant
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function orderBy(string $orderByConstant): static
|
||||
{
|
||||
$this->resultOrderings = match ($orderByConstant) {
|
||||
static::ORDERBY_LAST_MODIFIED => [
|
||||
['orderBy' => 'i.last_modified', 'direction' => static::ORDER_DIR_DESC]
|
||||
],
|
||||
static::ORDERBY_SEQUENCE => [
|
||||
['orderBy' => 'o.seq', 'direction' => static::ORDER_DIR_ASC]
|
||||
],
|
||||
static::ORDERBY_PUBLISHED_ISSUES => [
|
||||
['orderBy' => 'o.seq', 'direction' => static::ORDER_DIR_ASC],
|
||||
['orderBy' => 'currentIssue', 'direction' => static::ORDER_DIR_DESC],
|
||||
['orderBy' => 'i.date_published', 'direction' => static::ORDER_DIR_DESC]
|
||||
],
|
||||
static::ORDERBY_UNPUBLISHED_ISSUES => [
|
||||
['orderBy' => 'i.year', 'direction' => static::ORDER_DIR_ASC],
|
||||
['orderBy' => 'i.volume', 'direction' => static::ORDER_DIR_ASC],
|
||||
['orderBy' => 'i.number', 'direction' => static::ORDER_DIR_ASC]
|
||||
],
|
||||
static::ORDERBY_SHELF => [
|
||||
['orderBy' => static::ORDER_CURRENT_ISSUE, 'direction' => static::ORDER_DIR_DESC],
|
||||
['orderBy' => 'i.year', 'direction' => static::ORDER_DIR_ASC],
|
||||
['orderBy' => 'i.volume', 'direction' => static::ORDER_DIR_ASC],
|
||||
['orderBy' => 'i.number', 'direction' => static::ORDER_DIR_ASC]
|
||||
],
|
||||
default => throw new InvalidArgumentException('One of ORDERBY_* constants must be provided')
|
||||
};
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set published filter
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByPublished(bool $isPublished): static
|
||||
{
|
||||
$this->isPublished = $isPublished;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set volumes filter
|
||||
*
|
||||
* @param int[]|null $volumes
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByVolumes(?array $volumes): static
|
||||
{
|
||||
$this->volumes = $volumes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set volumes filter
|
||||
*
|
||||
* @param int[]|null $numbers
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByNumbers(?array $numbers): static
|
||||
{
|
||||
$this->numbers = $numbers;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set volumes filter
|
||||
*
|
||||
* @param int[]|null $years
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByYears(?array $years): static
|
||||
{
|
||||
$this->years = $years;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set urlPath filter
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByUrlPath(string $urlPath): static
|
||||
{
|
||||
$this->urlPath = $urlPath;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set titles filter
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByTitles(array $titles): static
|
||||
{
|
||||
$this->titles = $titles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to issues with these statuses
|
||||
*
|
||||
* @param array|null $statuses One or more of DOI::STATUS_* constants
|
||||
*
|
||||
*/
|
||||
public function filterByDoiStatuses(?array $statuses): static
|
||||
{
|
||||
$this->doiStatuses = $statuses;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to issues that do/don't have any DOIs assign to their sub objects
|
||||
*
|
||||
* @param array|null $enabledDoiTypes TYPE_* constants to consider when checking issue has DOIs
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filterByHasDois(?bool $hasDois, ?array $enabledDoiTypes = null): static
|
||||
{
|
||||
$this->hasDois = $hasDois;
|
||||
$this->enabledDoiTypes = $enabledDoiTypes === null ? [Repo::doi()::TYPE_ISSUE] : $enabledDoiTypes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set query search phrase
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function searchPhrase(?string $phrase): static
|
||||
{
|
||||
$this->searchPhrase = $phrase;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of objects retrieved
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function limit(?int $count): static
|
||||
{
|
||||
$this->count = $count;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the number of objects retrieved, for example to
|
||||
* retrieve the second page of contents
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function offset(?int $offset): static
|
||||
{
|
||||
$this->offset = $offset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getQueryBuilder(): Builder
|
||||
{
|
||||
$q = DB::table($this->dao->table, 'i')
|
||||
->select('i.*')
|
||||
->leftJoin('custom_issue_orders as o', 'o.issue_id', '=', 'i.issue_id');
|
||||
|
||||
// Issue titles (exact matches)
|
||||
$q->when(
|
||||
$this->titles !== null,
|
||||
fn (Builder $q) =>
|
||||
$q->whereIn(
|
||||
'i.issue_id',
|
||||
fn (Builder $q) =>
|
||||
$q->select('issue_id')
|
||||
->from($this->dao->settingsTable)
|
||||
->where('setting_name', '=', 'title')
|
||||
->whereIn('setting_value', $this->titles)
|
||||
)
|
||||
);
|
||||
|
||||
// Context
|
||||
// Never permit a query without a context_id unless the PKPApplication::CONTEXT_ID_ALL wildcard
|
||||
// has been set explicitly.
|
||||
if (!isset($this->contextIds)) {
|
||||
throw new Exception('Submissions can not be retrieved without a context id. Pass the Application::CONTEXT_ID_ALL wildcard to get submissions from any context.');
|
||||
} elseif (!in_array(PKPApplication::CONTEXT_ID_ALL, $this->contextIds)) {
|
||||
$q->whereIn('i.journal_id', $this->contextIds);
|
||||
}
|
||||
|
||||
// Issue IDs
|
||||
$q->when($this->issueIds !== null, fn (Builder $q) => $q->whereIn('i.issue_id', $this->issueIds));
|
||||
// Published
|
||||
$q->when($this->isPublished !== null, fn (Builder $q) => $q->where('i.published', '=', $this->isPublished ? 1 : 0));
|
||||
// Volumes
|
||||
$q->when($this->volumes !== null, fn (Builder $q) => $q->whereIn('i.volume', $this->volumes));
|
||||
// Numbers
|
||||
$q->when($this->numbers !== null, fn (Builder $q) => $q->whereIn('i.number', $this->numbers));
|
||||
// Years
|
||||
$q->when($this->years !== null, fn (Builder $q) => $q->whereIn('i.year', $this->years));
|
||||
// URL path
|
||||
$q->when($this->urlPath !== null, fn (Builder $q) => $q->where('i.url_path', '=', $this->urlPath));
|
||||
|
||||
// DOI statuses
|
||||
$q->when(
|
||||
$this->doiStatuses !== null,
|
||||
fn (Builder $q) =>
|
||||
$q->whereIn(
|
||||
'i.issue_id',
|
||||
fn (Builder $q) =>
|
||||
$q->select('i.issue_id')
|
||||
->from('issues as i')
|
||||
->leftJoin('dois as d', 'd.doi_id', '=', 'i.doi_id')
|
||||
->whereIn('d.status', $this->doiStatuses)
|
||||
)
|
||||
);
|
||||
|
||||
// By whether issue has DOI assigned
|
||||
$q->when(
|
||||
$this->hasDois !== null,
|
||||
fn (Builder $q) =>
|
||||
$q->whereIn(
|
||||
'i.issue_id',
|
||||
fn (Builder $q) =>
|
||||
$q->select('current_i.issue_id')
|
||||
->from('issues', 'current_i')
|
||||
->when(
|
||||
in_array(Repo::doi()::TYPE_ISSUE, $this->enabledDoiTypes),
|
||||
fn (Builder $q) =>
|
||||
$this->hasDois ? $q->whereNotNull('current_i.doi_id') : $q->whereNull('current_i.doi_id')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Search phrase
|
||||
if ($this->searchPhrase !== null) {
|
||||
$searchPhrase = $this->searchPhrase;
|
||||
|
||||
// Add support for searching for the volume, number and year
|
||||
// using the localized issue identification formats. In
|
||||
// en this will match Vol. 1. No. 1 (2018) against:
|
||||
// i.volume = 1 AND i.number = 1 AND i.year = 2018
|
||||
$volume = '';
|
||||
$volumeRegex = '/\b' . preg_quote(__('issue.vol'), '/') . '\s+(\d+)/';
|
||||
if (preg_match($volumeRegex, $searchPhrase, $matches)) {
|
||||
[$found, $volume] = $matches;
|
||||
$searchPhrase = str_replace($found, '', $searchPhrase);
|
||||
}
|
||||
$number = '';
|
||||
$numberRegex = '/\b' . preg_quote(__('issue.no'), '/') . '\s+(\S+)\b/';
|
||||
if (preg_match($numberRegex, $searchPhrase, $matches)) {
|
||||
[$found, $number] = $matches;
|
||||
$searchPhrase = str_replace($found, '', $searchPhrase);
|
||||
}
|
||||
$year = '';
|
||||
if (preg_match('/\((\d{4})\)/', $searchPhrase, $matches)) {
|
||||
[$found, $year] = $matches;
|
||||
$searchPhrase = str_replace($found, '', $searchPhrase);
|
||||
}
|
||||
$q->when(
|
||||
strlen($volume) || $number !== '' || $year !== '',
|
||||
fn (Builder $q) => $q->where(
|
||||
fn (Builder $q) => $q
|
||||
->when($volume !== '', fn (Builder $q) => $q->where('i.volume', '=', $volume))
|
||||
->when($number !== '', fn (Builder $q) => $q->where('i.number', '=', $number))
|
||||
->when($year !== '', fn (Builder $q) => $q->where('i.year', '=', $year))
|
||||
)
|
||||
);
|
||||
|
||||
$words = array_filter(array_unique(explode(' ', $searchPhrase)), 'strlen');
|
||||
if (count($words)) {
|
||||
$likePattern = DB::raw("CONCAT('%', LOWER(?), '%')");
|
||||
foreach ($words as $word) {
|
||||
$q->where(
|
||||
fn (Builder $q) => $q
|
||||
->whereIn(
|
||||
'i.issue_id',
|
||||
fn (Builder $q) =>
|
||||
$q->select('iss_t.issue_id')
|
||||
->from($this->dao->settingsTable, 'iss_t')
|
||||
->where('iss_t.setting_name', '=', 'title')
|
||||
->where(DB::raw('LOWER(iss_t.setting_value)'), 'LIKE', $likePattern)->addBinding($word)
|
||||
)
|
||||
->orWhereIn(
|
||||
'i.issue_id',
|
||||
fn (Builder $q) =>
|
||||
$q->select('iss_d.issue_id')
|
||||
->from($this->dao->settingsTable, 'iss_d')
|
||||
->where('iss_d.setting_name', '=', 'name')
|
||||
->where(DB::raw('LOWER(iss_d.setting_value)'), 'LIKE', $likePattern)->addBinding($word)
|
||||
)
|
||||
// Match any four-digit number to the year
|
||||
->when(ctype_digit($word) && strlen($word) === 4, fn (Builder $q) => $q->orWhere('i.year', '=', $word))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ordering for query-builder-based and legacy-based orderings
|
||||
$q->when($this->resultOrderings !== null, function (Builder $q) {
|
||||
foreach ($this->resultOrderings as $resultOrdering) {
|
||||
if ($resultOrdering['orderBy'] === static::ORDER_CURRENT_ISSUE) {
|
||||
// Custom query to order by current issue status from the journals table
|
||||
$q->leftJoin('journals as j', 'j.current_issue_id', '=', 'i.issue_id')
|
||||
->orderByRaw('CASE WHEN j.current_issue_id IS NOT NULL then 1 else 0 END ' . $resultOrdering['direction']);
|
||||
} else {
|
||||
$q->orderBy($resultOrdering['orderBy'], $resultOrdering['direction']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Limit and offset results for pagination
|
||||
$q->when($this->count !== null, fn (Builder $q) => $q->limit($this->count));
|
||||
$q->when($this->offset !== null, fn (Builder $q) => $q->offset($this->offset));
|
||||
|
||||
// Add app-specific query statements
|
||||
Hook::call('Issue::getMany::queryObject', [&$q, $this]);
|
||||
|
||||
return $q;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user