first commit
This commit is contained in:
@@ -0,0 +1,726 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @defgroup db DB
|
||||
* Implements basic database concerns such as connection abstraction.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/db/DAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class DAO
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @see DAORegistry
|
||||
*
|
||||
* @brief Operations for retrieving and modifying objects from a database.
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
use Generator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\cache\CacheManager;
|
||||
use PKP\core\JSONMessage;
|
||||
use PKP\plugins\Hook;
|
||||
|
||||
class DAO
|
||||
{
|
||||
public const SORT_DIRECTION_ASC = 1;
|
||||
public const SORT_DIRECTION_DESC = 2;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Initialize the database connection.
|
||||
*/
|
||||
public function __construct($callHooks = true)
|
||||
{
|
||||
if ($callHooks === true) {
|
||||
// Call hooks based on the object name. Results
|
||||
// in hook calls named e.g. "sessiondao::_Constructor"
|
||||
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
||||
if (Hook::run(strtolower_codesafe(end($classNameParts)) . '::_Constructor', [$this])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SELECT SQL statement.
|
||||
*
|
||||
* @param string $sql the SQL statement
|
||||
* @param array $params parameters for the SQL statement
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return Generator<int,object>
|
||||
*/
|
||||
public function retrieve($sql, $params = [], $callHooks = true)
|
||||
{
|
||||
if ($callHooks === true) {
|
||||
$trace = debug_backtrace();
|
||||
// Call hooks based on the calling entity, assuming
|
||||
// this method is only called by a subclass. Results
|
||||
// in hook calls named e.g. "sessiondao::_getsession"
|
||||
// (always lower case).
|
||||
$value = null;
|
||||
if (Hook::run(strtolower_codesafe($trace[1]['class'] . '::_' . $trace[1]['function']), [&$sql, &$params, &$value])) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return DB::cursor(DB::raw($sql)->getValue(), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SELECT SQL statement, returning rows in the range supplied.
|
||||
*
|
||||
* @param string $sql the SQL statement
|
||||
* @param array $params parameters for the SQL statement
|
||||
* @param DBResultRange $dbResultRange object describing the desired range
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function retrieveRange($sql, $params = [], $dbResultRange = null, $callHooks = true)
|
||||
{
|
||||
if ($callHooks === true) {
|
||||
$trace = debug_backtrace();
|
||||
// Call hooks based on the calling entity, assuming
|
||||
// this method is only called by a subclass. Results
|
||||
// in hook calls named e.g. "sessiondao::_getsession"
|
||||
$value = null;
|
||||
if (Hook::run(strtolower_codesafe($trace[1]['class'] . '::_' . $trace[1]['function']), [&$sql, &$params, &$dbResultRange, &$value])) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($dbResultRange && $dbResultRange->isValid()) {
|
||||
$sql .= ' LIMIT ' . (int) $dbResultRange->getCount();
|
||||
$offset = (int) $dbResultRange->getOffset();
|
||||
$offset += max(0, $dbResultRange->getPage() - 1) * (int) $dbResultRange->getCount();
|
||||
$sql .= ' OFFSET ' . $offset;
|
||||
}
|
||||
|
||||
return DB::cursor(DB::raw($sql), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of records in the supplied SQL statement (with optional bind parameters parameters)
|
||||
*
|
||||
* @param string $sql SQL query to be counted
|
||||
* @param array $params Optional SQL query bind parameters
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countRecords($sql, $params = [])
|
||||
{
|
||||
$result = $this->retrieve('SELECT COUNT(*) AS row_count FROM (' . $sql . ') AS count_subquery', $params);
|
||||
return $result->current()->row_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate SQL expressions into a single string.
|
||||
*
|
||||
* @param array ...$args SQL expressions (e.g. column names) to concatenate.
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function concat(...$args)
|
||||
{
|
||||
return 'CONCAT(' . join(',', $args) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an INSERT, UPDATE, or DELETE SQL statement.
|
||||
*
|
||||
* @param string $sql the SQL statement the execute
|
||||
* @param array $params an array of parameters for the SQL statement
|
||||
* @param bool $callHooks Whether or not to call hooks
|
||||
* @param bool $dieOnError Whether or not to die if an error occurs
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return int Affected row count
|
||||
*/
|
||||
public function update($sql, $params = [], $callHooks = true, $dieOnError = true)
|
||||
{
|
||||
if ($callHooks === true) {
|
||||
$trace = debug_backtrace();
|
||||
// Call hooks based on the calling entity, assuming
|
||||
// this method is only called by a subclass. Results
|
||||
// in hook calls named e.g. "sessiondao::_updateobject"
|
||||
// (all lowercase)
|
||||
$value = null;
|
||||
if (Hook::run(strtolower_codesafe($trace[1]['class'] . '::_' . $trace[1]['function']), [&$sql, &$params, &$value])) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return DB::affectingStatement($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a row in a table, replacing an existing row if necessary.
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $arrFields Associative array of colName => value
|
||||
* @param array $keyCols Array of column names that are keys
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*/
|
||||
public function replace($table, $arrFields, $keyCols)
|
||||
{
|
||||
$matchValues = array_filter($arrFields, fn ($key) => in_array($key, $keyCols), ARRAY_FILTER_USE_KEY);
|
||||
$additionalValues = array_filter($arrFields, fn ($key) => !in_array($key, $keyCols), ARRAY_FILTER_USE_KEY);
|
||||
DB::table($table)->updateOrInsert($matchValues, $additionalValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last ID inserted in an autonumbered field.
|
||||
*/
|
||||
protected function getInsertId(): int
|
||||
{
|
||||
return DB::getPdo()->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last ID inserted in an autonumbered field.
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*/
|
||||
public function _getInsertId(): int
|
||||
{
|
||||
return $this->getInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the caching directory for database results
|
||||
* NOTE: This is implemented as a GLOBAL setting and cannot
|
||||
* be set on a per-connection basis.
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*/
|
||||
protected function setCacheDir()
|
||||
{
|
||||
static $cacheDir;
|
||||
if (!isset($cacheDir)) {
|
||||
global $ADODB_CACHE_DIR;
|
||||
|
||||
$cacheDir = CacheManager::getFileCachePath() . '/_db';
|
||||
|
||||
$ADODB_CACHE_DIR = $cacheDir;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the system cache.
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*/
|
||||
public function flushCache()
|
||||
{
|
||||
$this->setCacheDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return datetime formatted for DB insertion.
|
||||
*
|
||||
* @param int|string $dt *nix timestamp or ISO datetime string
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function datetimeToDB($dt)
|
||||
{
|
||||
if ($dt === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (!ctype_digit((string) $dt)) {
|
||||
$dt = strtotime($dt);
|
||||
}
|
||||
return '\'' . date('Y-m-d H:i:s', $dt) . '\'';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return date formatted for DB insertion.
|
||||
*
|
||||
* @param int|string $d *nix timestamp or ISO date string
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dateToDB($d)
|
||||
{
|
||||
if ($d === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (!ctype_digit($d)) {
|
||||
$d = strtotime($d);
|
||||
}
|
||||
return '\'' . date('Y-m-d', $d) . '\'';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return datetime from DB as ISO datetime string.
|
||||
*
|
||||
* @param string $dt datetime from DB
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function datetimeFromDB($dt)
|
||||
{
|
||||
if ($dt === null) {
|
||||
return null;
|
||||
}
|
||||
return date('Y-m-d H:i:s', strtotime($dt));
|
||||
}
|
||||
/**
|
||||
* Return date from DB as ISO date string.
|
||||
*
|
||||
* @param string $d date from DB
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dateFromDB($d)
|
||||
{
|
||||
if ($d === null) {
|
||||
return null;
|
||||
}
|
||||
return date('Y-m-d', strtotime($d));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value from the database to a specific type
|
||||
*
|
||||
* @param mixed $value Value from the database
|
||||
* @param string $type Type from the database, eg `string`
|
||||
* @param bool $nullable True iff the value is allowed to be null
|
||||
*/
|
||||
public function convertFromDB($value, $type, $nullable = false)
|
||||
{
|
||||
if ($nullable && $value === null) {
|
||||
return null;
|
||||
}
|
||||
switch ($type) {
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
return (bool) $value;
|
||||
case 'int':
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'float':
|
||||
case 'number':
|
||||
return (float) $value;
|
||||
case 'object':
|
||||
case 'array':
|
||||
$decodedValue = json_decode($value, true);
|
||||
// FIXME: pkp/pkp-lib#6250 Remove after 3.3.x upgrade code is removed (see also pkp/pkp-lib#5772)
|
||||
if (!is_null($decodedValue)) {
|
||||
return $decodedValue;
|
||||
} else {
|
||||
return unserialize($value);
|
||||
}
|
||||
// no break
|
||||
case 'date':
|
||||
return strtotime($value);
|
||||
case 'string':
|
||||
default:
|
||||
// Nothing required.
|
||||
break;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of a value to be stored in the database
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType($value)
|
||||
{
|
||||
switch (gettype($value)) {
|
||||
case 'boolean':
|
||||
case 'bool':
|
||||
return 'bool';
|
||||
case 'integer':
|
||||
case 'int':
|
||||
return 'int';
|
||||
case 'double':
|
||||
case 'float':
|
||||
return 'float';
|
||||
case 'array':
|
||||
case 'object':
|
||||
return 'object';
|
||||
case 'string':
|
||||
default:
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a PHP variable into a string to be stored in the DB
|
||||
*
|
||||
* @param string $type
|
||||
* @param bool $nullable True iff the value is allowed to be null.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function convertToDB($value, &$type, $nullable = false)
|
||||
{
|
||||
if ($nullable && $value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type == null) {
|
||||
$type = $this->getType($value);
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'object':
|
||||
case 'array':
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
break;
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
// Cast to boolean, ensuring that string
|
||||
// "false" evaluates to boolean false
|
||||
$value = ($value && $value !== 'false') ? 1 : 0;
|
||||
break;
|
||||
case 'int':
|
||||
case 'integer':
|
||||
$value = (int) $value;
|
||||
break;
|
||||
case 'float':
|
||||
case 'number':
|
||||
$value = (float) $value;
|
||||
break;
|
||||
case 'date':
|
||||
if ($value !== null) {
|
||||
if (!is_numeric($value)) {
|
||||
$value = strtotime($value);
|
||||
}
|
||||
$value = date('Y-m-d H:i:s', $value);
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
default:
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the given parameter to an int, or leave it null.
|
||||
*
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function nullOrInt($value)
|
||||
{
|
||||
return (empty($value) ? null : (int) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of additional field names to store in this DAO.
|
||||
* This can be used to extend the table with virtual "columns",
|
||||
* typically using the ..._settings table.
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return array List of strings representing field names.
|
||||
*/
|
||||
public function getAdditionalFieldNames()
|
||||
{
|
||||
$returner = [];
|
||||
// Call hooks based on the calling entity, assuming
|
||||
// this method is only called by a subclass. Results
|
||||
// in hook calls named e.g. "sessiondao::getAdditionalFieldNames"
|
||||
// (class names lowercase)
|
||||
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
||||
Hook::run(strtolower_codesafe(end($classNameParts)) . '::getAdditionalFieldNames', [$this, &$returner]);
|
||||
|
||||
return $returner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale field names. Like getAdditionalFieldNames, but for
|
||||
* localized (multilingual) fields.
|
||||
*
|
||||
* @see getAdditionalFieldNames
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return array Array of string field names.
|
||||
*/
|
||||
public function getLocaleFieldNames()
|
||||
{
|
||||
$returner = [];
|
||||
// Call hooks based on the calling entity, assuming
|
||||
// this method is only called by a subclass. Results
|
||||
// in hook calls named e.g. "sessiondao::getLocaleFieldNames"
|
||||
// (class names lowercase)
|
||||
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
||||
Hook::run(strtolower_codesafe(end($classNameParts)) . '::getLocaleFieldNames', [$this, &$returner]);
|
||||
|
||||
return $returner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the settings table of a data object.
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param \PKP\core\DataObject $dataObject
|
||||
* @param array $idArray
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*/
|
||||
public function updateDataObjectSettings($tableName, $dataObject, $idArray)
|
||||
{
|
||||
// Initialize variables
|
||||
$idFields = array_keys($idArray);
|
||||
$idFields[] = 'locale';
|
||||
$idFields[] = 'setting_name';
|
||||
|
||||
// Build a data structure that we can process efficiently.
|
||||
$translated = $metadata = 1;
|
||||
$settings = !$metadata;
|
||||
$settingFields = [
|
||||
// Translated data
|
||||
$translated => [
|
||||
$settings => $this->getLocaleFieldNames(),
|
||||
$metadata => $dataObject->getLocaleMetadataFieldNames()
|
||||
],
|
||||
// Shared data
|
||||
!$translated => [
|
||||
$settings => $this->getAdditionalFieldNames(),
|
||||
$metadata => $dataObject->getAdditionalMetadataFieldNames()
|
||||
]
|
||||
];
|
||||
|
||||
// Loop over all fields and update them in the settings table
|
||||
$updateArray = $idArray;
|
||||
$noLocale = 0;
|
||||
$staleSettings = [];
|
||||
|
||||
foreach ($settingFields as $isTranslated => $fieldTypes) {
|
||||
foreach ($fieldTypes as $isMetadata => $fieldNames) {
|
||||
foreach ($fieldNames as $fieldName) {
|
||||
// Now we have the following control data:
|
||||
// - $isTranslated: true for translated data, false data shared between locales
|
||||
// - $isMetadata: true for metadata fields, false for normal settings
|
||||
// - $fieldName: the field in the data object to be updated
|
||||
if ($dataObject->hasData($fieldName)) {
|
||||
if ($isTranslated) {
|
||||
// Translated data comes in as an array
|
||||
// with the locale as the key.
|
||||
$values = $dataObject->getData($fieldName) ?? [];
|
||||
if (!is_array($values)) {
|
||||
// Inconsistent data: should have been an array
|
||||
assert(false);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Transform shared data into an array so that
|
||||
// we can handle them the same way as translated data.
|
||||
$values = [
|
||||
$noLocale => $dataObject->getData($fieldName)
|
||||
];
|
||||
}
|
||||
|
||||
// Loop over the values and update them in the database
|
||||
foreach ($values as $locale => $value) {
|
||||
$updateArray['locale'] = ($locale === $noLocale ? '' : $locale);
|
||||
$updateArray['setting_name'] = $fieldName;
|
||||
$updateArray['setting_type'] = null;
|
||||
// Convert the data value and implicitly set the setting type.
|
||||
$updateArray['setting_value'] = $this->convertToDB($value, $updateArray['setting_type']);
|
||||
$this->replace($tableName, $updateArray, $idFields);
|
||||
}
|
||||
} else {
|
||||
// Data is maintained "sparsely". Only set fields will be
|
||||
// recorded in the settings table. Fields that are not explicity set
|
||||
// in the data object will be deleted.
|
||||
$staleSettings[] = $fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove stale data
|
||||
if (count($staleSettings)) {
|
||||
$removeWhere = '';
|
||||
$removeParams = [];
|
||||
foreach ($idArray as $idField => $idValue) {
|
||||
if (!empty($removeWhere)) {
|
||||
$removeWhere .= ' AND ';
|
||||
}
|
||||
$removeWhere .= $idField . ' = ?';
|
||||
$removeParams[] = $idValue;
|
||||
}
|
||||
$removeWhere .= rtrim(' AND setting_name IN ( ' . str_repeat('? ,', count($staleSettings)), ',') . ')';
|
||||
$removeParams = array_merge($removeParams, $staleSettings);
|
||||
$removeSql = 'DELETE FROM ' . $tableName . ' WHERE ' . $removeWhere;
|
||||
$this->update($removeSql, $removeParams);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contents of the _settings table, storing entries in the specified
|
||||
* data object.
|
||||
*
|
||||
* @param string $tableName Settings table name
|
||||
* @param string $idFieldName Name of ID column
|
||||
* @param \PKP\core\DataObject $dataObject Object in which to store retrieved values
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*/
|
||||
public function getDataObjectSettings($tableName, $idFieldName, $idFieldValue, $dataObject)
|
||||
{
|
||||
if ($idFieldName !== null) {
|
||||
$sql = "SELECT * FROM {$tableName} WHERE {$idFieldName} = ?";
|
||||
$params = [$idFieldValue];
|
||||
} else {
|
||||
$sql = "SELECT * FROM {$tableName}";
|
||||
$params = [];
|
||||
}
|
||||
$result = $this->retrieve($sql, $params);
|
||||
foreach ($result as $row) {
|
||||
$dataObject->setData(
|
||||
$row->setting_name,
|
||||
$this->convertFromDB(
|
||||
$row->setting_value,
|
||||
$row->setting_type
|
||||
),
|
||||
empty($row->locale) ? null : $row->locale
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the driver for this connection.
|
||||
*
|
||||
* @param int $direction
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDirectionMapping($direction)
|
||||
{
|
||||
switch ($direction) {
|
||||
case self::SORT_DIRECTION_ASC:
|
||||
return 'ASC';
|
||||
case self::SORT_DIRECTION_DESC:
|
||||
return 'DESC';
|
||||
default:
|
||||
return 'ASC';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON message with an event that can be sent
|
||||
* to the client to refresh itself according to changes
|
||||
* in the DB.
|
||||
*
|
||||
* @param string $elementId (Optional) To refresh a single element
|
||||
* give the element ID here. Otherwise all elements will
|
||||
* be refreshed.
|
||||
* @param string $parentElementId (Optional) To refresh a single
|
||||
* element that is associated with another one give the parent
|
||||
* element ID here.
|
||||
* @param mixed $content (Optional) Additional content to pass back
|
||||
* to the handler of the JSON message.
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return JSONMessage
|
||||
*/
|
||||
public static function getDataChangedEvent($elementId = null, $parentElementId = null, $content = '')
|
||||
{
|
||||
// Create the event data.
|
||||
$eventData = null;
|
||||
if ($elementId) {
|
||||
$eventData = [$elementId];
|
||||
if (strlen($parentElementId) > 0) {
|
||||
$eventData['parentElementId'] = $parentElementId;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and render the JSON message with the
|
||||
// event to be triggered on the client side.
|
||||
$json = new JSONMessage(true, $content);
|
||||
$json->setEvent('dataChanged', $eventData);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a passed date (in English textual datetime)
|
||||
* to Y-m-d H:i:s format, used in database.
|
||||
*
|
||||
* @param string $date Any English textual datetime.
|
||||
* @param int $defaultNumWeeks If passed and date is null,
|
||||
* used to calculate a data in future from today.
|
||||
* @param bool $acceptPastDate Will not accept past dates,
|
||||
* returning today if false and the passed date
|
||||
* is in the past.
|
||||
*
|
||||
* @deprecated 3.4
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function formatDateToDB($date, $defaultNumWeeks = null, $acceptPastDate = true)
|
||||
{
|
||||
$today = getDate();
|
||||
$todayTimestamp = mktime(0, 0, 0, $today['mon'], $today['mday'], $today['year']);
|
||||
if ($date != null) {
|
||||
$dateParts = explode('-', $date);
|
||||
|
||||
// If we don't accept past dates...
|
||||
if (!$acceptPastDate && $todayTimestamp > strtotime($date)) {
|
||||
// ... return today.
|
||||
return date('Y-m-d H:i:s', $todayTimestamp);
|
||||
} else {
|
||||
// Return the passed date.
|
||||
return date('Y-m-d H:i:s', mktime(0, 0, 0, $dateParts[1], $dateParts[2], $dateParts[0]));
|
||||
}
|
||||
} elseif (isset($defaultNumWeeks)) {
|
||||
// Add the equivalent of $numWeeks weeks, measured in seconds, to $todaysTimestamp.
|
||||
$numWeeks = max((int) $defaultNumWeeks, 2);
|
||||
$newDueDateTimestamp = $todayTimestamp + ($numWeeks * 7 * 24 * 60 * 60);
|
||||
return date('Y-m-d H:i:s', $newDueDateTimestamp);
|
||||
} else {
|
||||
// Either the date or the defaultNumWeeks must be set
|
||||
assert(false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\DAO', '\DAO');
|
||||
define('SORT_DIRECTION_ASC', DAO::SORT_DIRECTION_ASC);
|
||||
define('SORT_DIRECTION_DESC', DAO::SORT_DIRECTION_DESC);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/db/DAORegistry.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class DAORegistry
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @see DAO
|
||||
*
|
||||
* @brief Maintains a static list of DAO objects so each DAO is instantiated only once.
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
use APP\core\Application;
|
||||
use PKP\core\Registry;
|
||||
|
||||
class DAORegistry
|
||||
{
|
||||
/**
|
||||
* Get the current list of registered DAOs.
|
||||
* This returns a reference to the static hash used to
|
||||
* store all DAOs currently instantiated by the system.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function &getDAOs()
|
||||
{
|
||||
$daos = & Registry::get('daos', true, []);
|
||||
return $daos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new DAO with the system.
|
||||
*
|
||||
* @param string $name The name of the DAO to register
|
||||
* @param object $dao A reference to the DAO to be registered
|
||||
*
|
||||
* @return object A reference to previously-registered DAO of the same
|
||||
* name, if one was already registered; null otherwise
|
||||
*/
|
||||
public static function registerDAO($name, $dao)
|
||||
{
|
||||
$daos = & DAORegistry::getDAOs();
|
||||
|
||||
$returner = null;
|
||||
|
||||
if (isset($daos[$name])) {
|
||||
$returner = $daos[$name];
|
||||
}
|
||||
|
||||
$daos[$name] = $dao;
|
||||
return $returner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a reference to the specified DAO.
|
||||
*
|
||||
* @param string $name the class name of the requested DAO
|
||||
*
|
||||
* @return DAO
|
||||
*/
|
||||
public static function &getDAO($name)
|
||||
{
|
||||
$daos = & DAORegistry::getDAOs();
|
||||
if (!isset($daos[$name])) {
|
||||
// Import the required DAO class.
|
||||
$application = Application::get();
|
||||
$className = $application->getQualifiedDAOName($name);
|
||||
if (!$className) {
|
||||
throw new \Exception('Unrecognized DAO ' . $name . '!');
|
||||
}
|
||||
|
||||
// Only instantiate each class of DAO a single time
|
||||
$daos[$name] = new $className();
|
||||
}
|
||||
|
||||
return $daos[$name];
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\DAORegistry', '\DAORegistry');
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/db/DAOResultFactory.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class DAOResultFactory
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @brief Wrapper around Enumerable providing "factory" features for generating
|
||||
* objects from DAOs.
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
use APP\submission\DAO;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use PKP\core\ItemIterator;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* @template T of \PKP\core\DataObject
|
||||
* @extends ItemIterator<mixed,T>
|
||||
*/
|
||||
class DAOResultFactory extends ItemIterator
|
||||
{
|
||||
/** @var DAO The DAO used to create objects */
|
||||
public $dao;
|
||||
|
||||
/** @var string The name of the DAO's factory function (to be called with an associative array of values) */
|
||||
public $functionName;
|
||||
|
||||
/**
|
||||
* @var array an array of primary key field names that uniquely
|
||||
* identify a result row in the record set.
|
||||
*/
|
||||
public $idFields;
|
||||
|
||||
/** @var \Generator<int,object>|Enumerable<int,object> The results to be wrapped around */
|
||||
public $records;
|
||||
|
||||
/**
|
||||
* @var string|null Fetch SQL
|
||||
*/
|
||||
public $sql;
|
||||
|
||||
/**
|
||||
* @var array|null Fetch parameters
|
||||
*/
|
||||
public $params;
|
||||
|
||||
/**
|
||||
* @var DBResultRange|null $rangeInfo Range information, if specified.
|
||||
*/
|
||||
public $rangeInfo;
|
||||
|
||||
/**
|
||||
* @var bool Does $functionName expect each record to be converted to an array
|
||||
*/
|
||||
public $expectsArray = true;
|
||||
|
||||
/** @var ?int Cached row count */
|
||||
private $rowCount = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Initialize the DAOResultFactory
|
||||
*
|
||||
* @param object $records ADO record set, Generator, or Enumerable
|
||||
* @param object $dao DAO class for factory
|
||||
* @param string $functionName The function to call on $dao to create an object
|
||||
* @param array $idFields an array of primary key field names that uniquely identify a result row in the record set. Should be data object _data array key, not database column name
|
||||
* @param string $sql Optional SQL query used to generate paged result set. Necessary when total row counts will be needed (e.g. when paging). WARNING: New code should not use this.
|
||||
* @param array $params Optional parameters for SQL query used to generate paged result set. Necessary when total row counts will be needed (e.g. when paging). WARNING: New code should not use this.
|
||||
* @param ?DBResultRange $rangeInfo Optional pagination information. WARNING: New code should not use this.
|
||||
*/
|
||||
public function __construct($records, $dao, $functionName, $idFields = [], $sql = null, $params = [], $rangeInfo = null)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->functionName = $functionName;
|
||||
$this->dao = $dao;
|
||||
$this->idFields = $idFields;
|
||||
$this->records = $records;
|
||||
$this->sql = $sql;
|
||||
$this->params = $params;
|
||||
$this->rangeInfo = $rangeInfo;
|
||||
|
||||
// Determine if the "fromRow" method expects to receive an array or a stdClass.
|
||||
// EntityDAOs expect an object. DAOs that extend PKP\db\DAO expect an array.
|
||||
$reflector = new ReflectionClass(get_class($this->dao));
|
||||
if ($reflector->hasMethod($this->functionName)) {
|
||||
$params = $reflector->getMethod($this->functionName)->getParameters();
|
||||
if (!empty($params) && $params[0]->hasType() && $params[0]->getType()->getName() === 'object') {
|
||||
$this->expectsArray = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the object representing the next row.
|
||||
*
|
||||
* @return ?T
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
if ($this->records == null) {
|
||||
return $this->records;
|
||||
}
|
||||
|
||||
$row = null;
|
||||
$functionName = $this->functionName;
|
||||
$dao = $this->dao;
|
||||
|
||||
if ($this->records instanceof \Generator) {
|
||||
$row = $this->records->current();
|
||||
$this->records->next();
|
||||
} elseif ($this->records instanceof Collection) {
|
||||
$row = $this->records->shift();
|
||||
} else {
|
||||
throw new \Exception('Unsupported record set type (' . join(', ', class_implements($this->records)) . ')');
|
||||
}
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
if ($this->expectsArray) {
|
||||
$row = (array) $row;
|
||||
}
|
||||
return $dao->$functionName($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc ItemIterator::count()
|
||||
*/
|
||||
public function getCount()
|
||||
{
|
||||
if ($this->sql === null) {
|
||||
throw new \Exception('DAOResultFactory instances cannot be counted unless supplied in constructor (DAO ' . $this->dao::class . ')!');
|
||||
}
|
||||
// EntityDAOs do not support the countRecords method, but it can
|
||||
// be accessed through an instance of PKP\db\DAO attached to them
|
||||
$dao = property_exists($this->dao, 'deprecatedDao') ? $this->dao->deprecatedDao : $this->dao;
|
||||
return $this->rowCount ??= $dao->countRecords($this->sql, $this->params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next row, with key.
|
||||
*
|
||||
* @param null|mixed $idField
|
||||
*
|
||||
* @return ?array{mixed,T} ($key, $value)
|
||||
*/
|
||||
public function nextWithKey($idField = null)
|
||||
{
|
||||
$result = $this->next();
|
||||
if ($idField) {
|
||||
assert($result instanceof \PKP\core\DataObject);
|
||||
$key = $result->getData($idField);
|
||||
} elseif (empty($this->idFields)) {
|
||||
$key = null;
|
||||
} else {
|
||||
assert($result instanceof \PKP\core\DataObject && is_array($this->idFields));
|
||||
$key = '';
|
||||
foreach ($this->idFields as $idField) {
|
||||
assert(!is_null($result->getData($idField)));
|
||||
if (!empty($key)) {
|
||||
$key .= '-';
|
||||
}
|
||||
$key .= (string)$result->getData($idField);
|
||||
}
|
||||
}
|
||||
return [$key, $result];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page number of a set that this iterator represents.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->rangeInfo->getPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of pages in this set.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPageCount()
|
||||
{
|
||||
return ceil($this->getCount() / $this->rangeInfo->getCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean indicating whether or not we've reached the end of results
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function eof()
|
||||
{
|
||||
if ($this->records == null) {
|
||||
return true;
|
||||
}
|
||||
/** @var DAOResultIterator */
|
||||
$records = $this->records;
|
||||
return !$records->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true iff the result list was empty.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasEmpty()
|
||||
{
|
||||
return $this->getCount() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this iterator to an array.
|
||||
*
|
||||
* @return T[]
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$returner = [];
|
||||
while ($row = $this->next()) {
|
||||
$returner[] = $row;
|
||||
}
|
||||
return $returner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Iterator for this DAOResultFactory.
|
||||
*
|
||||
* @return DAOResultIterator<T>
|
||||
*/
|
||||
public function toIterator()
|
||||
{
|
||||
return new DAOResultIterator($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this iterator to an associative array by database ID.
|
||||
*
|
||||
* @return array<array-key,T>
|
||||
*/
|
||||
public function toAssociativeArray($idField = 'id')
|
||||
{
|
||||
$returner = [];
|
||||
while ($row = $this->next()) {
|
||||
$returner[$row->getData($idField)] = $row;
|
||||
}
|
||||
return $returner;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\DAOResultFactory', '\DAOResultFactory');
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/db/DAOResultIterator.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class DAOResultIterator
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @brief Wrapper around a DAOResultFactory providing a proper PHP Iterator implementation
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
/**
|
||||
* @template T of \PKP\core\DataObject
|
||||
* @implements \Iterator<int,T>
|
||||
*/
|
||||
class DAOResultIterator implements \Iterator, \Countable
|
||||
{
|
||||
/** @var DAOResultFactory<T> */
|
||||
public $_resultFactory;
|
||||
|
||||
/** @var T Current return value data object. */
|
||||
public $_current = null;
|
||||
|
||||
/** @var int $_i 0-based index of current data object. */
|
||||
public $_i = 0;
|
||||
|
||||
/**
|
||||
* Create an Iterator for the specified DAOResultFactory.
|
||||
* @param DAOResultFactory<T> $resultFactory
|
||||
*/
|
||||
public function __construct($resultFactory)
|
||||
{
|
||||
$this->_resultFactory = $resultFactory;
|
||||
$this->_current = $this->_resultFactory->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc \Iterator::current
|
||||
*/
|
||||
public function current(): mixed
|
||||
{
|
||||
return $this->_current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the 0-based index for the current object.
|
||||
* Note that this is NOT the DataObject's ID -- for that, call
|
||||
* getId() on the current element.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function key(): mixed
|
||||
{
|
||||
if (!$this->_current) {
|
||||
return null;
|
||||
}
|
||||
return $this->_i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc \Iterator::next()
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
$this->_current = $this->_resultFactory->next();
|
||||
$this->_i++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the DAOResultFactory to the beginning. WARNING that this
|
||||
* operation is not arbitrarily supported -- it can only be called
|
||||
* before the first call to `next()`.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
if ($this->_i != 0) {
|
||||
throw new \Exception('DAOResultIterator currently does not support rewind() once iteration has started.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc \Iterator::valid()
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return ($this->_current !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc \Countable::count()
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->_resultFactory->getCount();
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\DAOResultIterator', '\DAOResultIterator');
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/db/DBDataXMLParser.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class DBDataXMLParser
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @brief Class to import and export database data from an XML format.
|
||||
* See dbscripts/xml/dtd/xmldata.dtd for the XML schema used.
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use PKP\config\Config;
|
||||
use PKP\xml\PKPXMLParser;
|
||||
use PKP\xml\XMLNode;
|
||||
|
||||
class DBDataXMLParser
|
||||
{
|
||||
/** @var array the array of parsed SQL statements */
|
||||
public $sql;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->sql = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an XML data file into SQL statements.
|
||||
*
|
||||
* @param string $file path to the XML file to parse
|
||||
*
|
||||
* @return array the array of SQL statements parsed
|
||||
*/
|
||||
public function parseData($file)
|
||||
{
|
||||
$this->sql = [];
|
||||
$parser = new PKPXMLParser();
|
||||
$tree = $parser->parse($file);
|
||||
if (!$tree) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$allTables = DB::getDoctrineSchemaManager()->listTableNames();
|
||||
|
||||
foreach ($tree->getChildren() as $type) {
|
||||
switch ($type->getName()) {
|
||||
case 'table':
|
||||
$fieldDefaultValues = [];
|
||||
|
||||
// Match table element
|
||||
foreach ($type->getChildren() as $row) {
|
||||
switch ($row->getName()) {
|
||||
case 'row':
|
||||
// Match a row element
|
||||
$fieldValues = [];
|
||||
|
||||
foreach ($row->getChildren() as $field) {
|
||||
// Get the field names and values for this INSERT
|
||||
[$fieldName, $value] = $this->_getFieldData($field);
|
||||
$fieldValues[$fieldName] = $value;
|
||||
}
|
||||
|
||||
$fieldValues = array_merge($fieldDefaultValues, $fieldValues);
|
||||
|
||||
if (count($fieldValues) > 0) {
|
||||
$this->sql[] = sprintf(
|
||||
'INSERT INTO %s (%s) VALUES (%s)',
|
||||
$type->getAttribute('name'),
|
||||
join(', ', array_keys($fieldValues)),
|
||||
join(', ', array_values($fieldValues))
|
||||
);
|
||||
}
|
||||
break;
|
||||
default: assert(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'sql':
|
||||
// Match sql element (set of SQL queries)
|
||||
foreach ($type->getChildren() as $child) {
|
||||
switch ($child->getName()) {
|
||||
case 'drop':
|
||||
$table = $child->getAttribute('table');
|
||||
$column = $child->getAttribute('column');
|
||||
if ($column) {
|
||||
$this->sql = array_merge($this->sql, array_column(DB::pretend(function () use ($table, $column) {
|
||||
Schema::table($table, function (Blueprint $table) use ($column) {
|
||||
$table->dropColumn($column);
|
||||
});
|
||||
}), 'query'));
|
||||
} else {
|
||||
$this->sql = array_merge($this->sql, array_column(DB::pretend(function () use ($table) {
|
||||
Schema::drop($table);
|
||||
}), 'query'));
|
||||
}
|
||||
break;
|
||||
case 'rename':
|
||||
$table = $child->getAttribute('table');
|
||||
$column = $child->getAttribute('column');
|
||||
$to = $child->getAttribute('to');
|
||||
if ($column) {
|
||||
// Rename a column.
|
||||
$this->sql = array_merge($this->sql, array_column(DB::pretend(function () use ($table, $column, $to) {
|
||||
Schema::table($table, function (Blueprint $table) use ($column, $to) {
|
||||
$table->renameColumn($column, $to);
|
||||
});
|
||||
}), 'query'));
|
||||
} else {
|
||||
// Rename the table.
|
||||
$this->sql = array_merge($this->sql, array_column(DB::pretend(function () use ($table, $to) {
|
||||
Schema::rename($table, $to);
|
||||
}), 'query'));
|
||||
}
|
||||
break;
|
||||
case 'dropindex':
|
||||
$table = $child->getAttribute('table');
|
||||
$index = $child->getAttribute('index');
|
||||
if (!$table || !$index) {
|
||||
throw new Exception('dropindex called without table or index');
|
||||
}
|
||||
|
||||
$schemaManager = DB::getDoctrineSchemaManager();
|
||||
if ($child->getAttribute('ifexists') && !in_array($index, array_keys($schemaManager->listTableIndexes($table)))) {
|
||||
break;
|
||||
}
|
||||
$this->sql = array_merge($this->sql, array_column(DB::pretend(function () use ($table, $index) {
|
||||
Schema::table($table, function (Blueprint $table) use ($index) {
|
||||
$table->dropIndex($index);
|
||||
});
|
||||
}), 'query'));
|
||||
break;
|
||||
case 'query':
|
||||
// If a "driver" attribute is specified, multiple drivers can be
|
||||
// specified with a comma separator.
|
||||
$driver = $child->getAttribute('driver');
|
||||
if (empty($driver) || in_array(Config::getVar('database', 'driver'), array_map('trim', explode(',', $driver)))) {
|
||||
$this->sql[] = $child->getValue();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the parsed SQL statements.
|
||||
*
|
||||
* @param bool $continueOnError continue to execute remaining statements if a failure occurs
|
||||
*
|
||||
* @return bool success
|
||||
*/
|
||||
public function executeData($continueOnError = false)
|
||||
{
|
||||
foreach ($this->sql as $stmt) {
|
||||
try {
|
||||
DB::statement($stmt);
|
||||
} catch (Exception $e) {
|
||||
if (!$continueOnError) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parsed SQL statements.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSQL()
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string to be appear as a value in an SQL INSERT statement.
|
||||
*
|
||||
* @param string $str
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function quoteString($str)
|
||||
{
|
||||
return DB::getPdo()->quote($str);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Private helper methods
|
||||
//
|
||||
/**
|
||||
* retrieve a field name and value from a field node
|
||||
*
|
||||
* @param XMLNode $fieldNode
|
||||
*
|
||||
* @return array an array with two entries: the field
|
||||
* name and the field value
|
||||
*/
|
||||
public function _getFieldData($fieldNode)
|
||||
{
|
||||
$fieldName = $fieldNode->getAttribute('name');
|
||||
$fieldValue = $fieldNode->getValue();
|
||||
|
||||
// Is this field empty? If so: do we want NULL or
|
||||
// an empty string?
|
||||
$isEmpty = $fieldNode->getAttribute('null');
|
||||
if (!is_null($isEmpty)) {
|
||||
assert(is_null($fieldValue));
|
||||
switch ($isEmpty) {
|
||||
case 1:
|
||||
$fieldValue = null;
|
||||
break;
|
||||
|
||||
case 0:
|
||||
$fieldValue = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Translate null to 'NULL' for SQL use.
|
||||
if (is_null($fieldValue)) {
|
||||
$fieldValue = 'NULL';
|
||||
} else {
|
||||
// Quote the value.
|
||||
if (!is_numeric($fieldValue)) {
|
||||
$fieldValue = $this->quoteString($fieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
return [$fieldName, $fieldValue];
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\DBDataXMLParser', '\DBDataXMLParser');
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/db/DBResultRange.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class DBResultRange
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @brief Container class for range information when retrieving a result set.
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
class DBResultRange
|
||||
{
|
||||
/** @var int The number of items to display */
|
||||
public $count;
|
||||
|
||||
/** @var int The number of pages to skip */
|
||||
public $page;
|
||||
|
||||
/** @var int Optional offset if pagination is not used. */
|
||||
public $offset;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Initialize the DBResultRange.
|
||||
*
|
||||
* @param null|mixed $offset
|
||||
*/
|
||||
public function __construct($count, $page = 1, $offset = null)
|
||||
{
|
||||
$this->count = $count;
|
||||
$this->page = $page;
|
||||
$this->offset = $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the DBResultRange is valid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return (($this->count > 0) && ($this->page >= 0))
|
||||
|| ($this->count > 0 && !is_null($this->offset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of pages to skip.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the count of pages to skip.
|
||||
*
|
||||
* @param int $page
|
||||
*/
|
||||
public function setPage($page)
|
||||
{
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of items in this range to display.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the count of items in this range to display.
|
||||
*
|
||||
* @param int $count
|
||||
*/
|
||||
public function setCount($count)
|
||||
{
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of items in this range to display.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOffset()
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the offset of items in this range to display.
|
||||
*
|
||||
* @param int $offset
|
||||
*/
|
||||
public function setOffset($offset)
|
||||
{
|
||||
$this->offset = $offset;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\DBResultRange', '\DBResultRange');
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
/**
|
||||
* @file classes/db/SchemaDAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class SchemaDAO
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @brief A base class for DAOs which rely on a json-schema file to define
|
||||
* the data object.
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
use APP\core\Services;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* @template T of \PKP\core\DataObject
|
||||
*/
|
||||
abstract class SchemaDAO extends DAO
|
||||
{
|
||||
/** @var string One of the SCHEMA_... constants */
|
||||
public $schemaName;
|
||||
|
||||
/** @var string The name of the primary table for this object */
|
||||
public $tableName;
|
||||
|
||||
/** @var string The name of the settings table for this object */
|
||||
public $settingsTableName;
|
||||
|
||||
/** @var string The column name for the object id in primary and settings tables */
|
||||
public $primaryKeyColumn;
|
||||
|
||||
/** @var array Maps schema properties for the primary table to their column names */
|
||||
public $primaryTableColumns = [];
|
||||
|
||||
/**
|
||||
* Create a new \PKP\core\DataObject of the appropriate class
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
abstract public function newDataObject();
|
||||
|
||||
/**
|
||||
* Retrieve an object by ID
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return ?T
|
||||
*/
|
||||
public function getById($objectId)
|
||||
{
|
||||
$result = $this->retrieve(
|
||||
'SELECT * FROM ' . $this->tableName . ' WHERE ' . $this->primaryKeyColumn . ' = ?',
|
||||
[(int) $objectId]
|
||||
);
|
||||
|
||||
$row = (array) $result->current();
|
||||
return $row ? $this->_fromRow($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new object
|
||||
*
|
||||
* @param T $object The object to insert into the database
|
||||
*
|
||||
* @return int The new object's id
|
||||
*/
|
||||
public function insertObject($object)
|
||||
{
|
||||
$schemaService = Services::get('schema');
|
||||
$schema = $schemaService->get($this->schemaName);
|
||||
$sanitizedProps = $schemaService->sanitize($this->schemaName, $object->_data);
|
||||
|
||||
$primaryDbProps = $this->_getPrimaryDbProps($object);
|
||||
|
||||
if (empty($primaryDbProps)) {
|
||||
throw new Exception('Tried to insert ' . get_class($object) . ' without any properties for the ' . $this->tableName . ' table.');
|
||||
}
|
||||
|
||||
DB::table($this->tableName)->insert($primaryDbProps);
|
||||
$object->setId(DB::getPdo()->lastInsertId());
|
||||
|
||||
// Add additional properties to settings table if they exist
|
||||
if (count($sanitizedProps) !== count($primaryDbProps)) {
|
||||
$columns = [$this->primaryKeyColumn, 'locale', 'setting_name', 'setting_value'];
|
||||
$columnsList = join(', ', $columns);
|
||||
$bindList = join(', ', array_fill(0, count($columns), '?'));
|
||||
foreach ($schema->properties as $propName => $propSchema) {
|
||||
if (!isset($sanitizedProps[$propName]) || array_key_exists($propName, $this->primaryTableColumns)) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($propSchema->multilingual)) {
|
||||
foreach ($sanitizedProps[$propName] as $localeKey => $localeValue) {
|
||||
$this->update("INSERT INTO {$this->settingsTableName} ({$columnsList}) VALUES ({$bindList})", [
|
||||
$object->getId(),
|
||||
$localeKey,
|
||||
$propName,
|
||||
$this->convertToDB($localeValue, $schema->properties->{$propName}->type),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$this->update("INSERT INTO {$this->settingsTableName} ({$columnsList}) VALUES ({$bindList})", [
|
||||
$object->getId(),
|
||||
'',
|
||||
$propName,
|
||||
$this->convertToDB($sanitizedProps[$propName], $schema->properties->{$propName}->type),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $object->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an object
|
||||
*
|
||||
* When updating an object, we remove table rows for any settings which are
|
||||
* no longer present.
|
||||
*
|
||||
* Multilingual fields are an exception to this. When a locale key is missing
|
||||
* from the object, it is not removed from the database. This is to ensure
|
||||
* that data from a locale which is later disabled is not automatically
|
||||
* removed from the database, and will appear if the locale is re-enabled.
|
||||
*
|
||||
* To delete a value for a locale key, a null value must be passed.
|
||||
*
|
||||
* @param T $object The object to insert into the database
|
||||
*/
|
||||
public function updateObject($object)
|
||||
{
|
||||
$schemaService = Services::get('schema');
|
||||
$schema = $schemaService->get($this->schemaName);
|
||||
$sanitizedProps = $schemaService->sanitize($this->schemaName, $object->_data);
|
||||
|
||||
$primaryDbProps = $this->_getPrimaryDbProps($object);
|
||||
|
||||
$set = join('=?,', array_keys($primaryDbProps)) . '=?';
|
||||
$this->update(
|
||||
"UPDATE {$this->tableName} SET {$set} WHERE {$this->primaryKeyColumn} = ?",
|
||||
array_merge(array_values($primaryDbProps), [$object->getId()])
|
||||
);
|
||||
|
||||
$deleteSettings = [];
|
||||
foreach ($schema->properties as $propName => $propSchema) {
|
||||
if (array_key_exists($propName, $this->primaryTableColumns)) {
|
||||
continue;
|
||||
} elseif (!isset($sanitizedProps[$propName])) {
|
||||
$deleteSettings[] = $propName;
|
||||
continue;
|
||||
}
|
||||
if (!empty($propSchema->multilingual)) {
|
||||
foreach ($sanitizedProps[$propName] as $localeKey => $localeValue) {
|
||||
// Delete rows with a null value
|
||||
if (is_null($localeValue)) {
|
||||
$this->update("DELETE FROM {$this->settingsTableName} WHERE {$this->primaryKeyColumn} = ? AND setting_name = ? AND locale = ?", [
|
||||
$object->getId(),
|
||||
$propName,
|
||||
$localeKey,
|
||||
]);
|
||||
} else {
|
||||
DB::table($this->settingsTableName)->updateOrInsert(
|
||||
[$this->primaryKeyColumn => $object->getId(), 'locale' => $localeKey, 'setting_name' => $propName],
|
||||
['setting_value' => $this->convertToDB($localeValue, $schema->properties->{$propName}->type)]
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DB::table($this->settingsTableName)->updateOrInsert(
|
||||
[$this->primaryKeyColumn => $object->getId(), 'locale' => '', 'setting_name' => $propName],
|
||||
['setting_value' => $this->convertToDB($sanitizedProps[$propName], $schema->properties->{$propName}->type)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($deleteSettings)) {
|
||||
$deleteSettingNames = join(',', array_map(function ($settingName) {
|
||||
return "'{$settingName}'";
|
||||
}, $deleteSettings));
|
||||
$this->update(
|
||||
"DELETE FROM {$this->settingsTableName} WHERE {$this->primaryKeyColumn} = ? AND setting_name in ({$deleteSettingNames})",
|
||||
[$object->getId()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object
|
||||
*
|
||||
* A wrapper function for SchemaDAO::deleteObjectById().
|
||||
*
|
||||
* @param T $object The object to insert into the database
|
||||
*/
|
||||
public function deleteObject($object)
|
||||
{
|
||||
$this->deleteById($object->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object by its ID
|
||||
*
|
||||
* @param int $objectId
|
||||
*/
|
||||
public function deleteById($objectId)
|
||||
{
|
||||
$this->update(
|
||||
"DELETE FROM {$this->tableName} WHERE {$this->primaryKeyColumn} = ?",
|
||||
[(int) $objectId]
|
||||
);
|
||||
$this->update(
|
||||
"DELETE FROM {$this->settingsTableName} WHERE {$this->primaryKeyColumn} = ?",
|
||||
[(int) $objectId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a \PKP\core\DataObject from a result row
|
||||
*
|
||||
* @param array $primaryRow The result row from the primary table lookup
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
public function _fromRow($primaryRow)
|
||||
{
|
||||
$schemaService = Services::get('schema');
|
||||
$schema = $schemaService->get($this->schemaName);
|
||||
|
||||
$object = $this->newDataObject();
|
||||
|
||||
foreach ($this->primaryTableColumns as $propName => $column) {
|
||||
if (isset($primaryRow[$column])) {
|
||||
$object->setData(
|
||||
$propName,
|
||||
$this->convertFromDb($primaryRow[$column], $schema->properties->{$propName}->type)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->retrieve(
|
||||
"SELECT * FROM {$this->settingsTableName} WHERE {$this->primaryKeyColumn} = ?",
|
||||
[$primaryRow[$this->primaryKeyColumn]]
|
||||
);
|
||||
|
||||
foreach ($result as $settingRow) {
|
||||
$settingRow = (array) $settingRow;
|
||||
if (!empty($schema->properties->{$settingRow['setting_name']})) {
|
||||
$object->setData(
|
||||
$settingRow['setting_name'],
|
||||
$this->convertFromDB(
|
||||
$settingRow['setting_value'],
|
||||
$schema->properties->{$settingRow['setting_name']}->type
|
||||
),
|
||||
empty($settingRow['locale']) ? null : $settingRow['locale']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to compile the key/value set for the primary table
|
||||
*
|
||||
* @param T $object
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function _getPrimaryDbProps($object)
|
||||
{
|
||||
$schema = Services::get('schema')->get($this->schemaName);
|
||||
$sanitizedProps = Services::get('schema')->sanitize($this->schemaName, $object->_data);
|
||||
|
||||
$primaryDbProps = [];
|
||||
foreach ($this->primaryTableColumns as $propName => $columnName) {
|
||||
if ($propName !== 'id' && array_key_exists($propName, $sanitizedProps)) {
|
||||
// If the value is null and the prop is nullable, leave it null
|
||||
if (is_null($sanitizedProps[$propName])
|
||||
&& isset($schema->properties->{$propName}->validation)
|
||||
&& in_array('nullable', $schema->properties->{$propName}->validation)) {
|
||||
$primaryDbProps[$columnName] = null;
|
||||
|
||||
// Convert empty string values for DATETIME columns into null values
|
||||
// because an empty string can not be saved to a DATETIME column
|
||||
} elseif (array_key_exists($columnName, $sanitizedProps)
|
||||
&& $sanitizedProps[$columnName] === ''
|
||||
&& isset($schema->properties->{$propName}->validation)
|
||||
&& (
|
||||
in_array('date_format:Y-m-d H:i:s', $schema->properties->{$propName}->validation)
|
||||
|| in_array('date_format:Y-m-d', $schema->properties->{$propName}->validation)
|
||||
)
|
||||
) {
|
||||
$primaryDbProps[$columnName] = null;
|
||||
} else {
|
||||
$primaryDbProps[$columnName] = $this->convertToDB($sanitizedProps[$propName], $schema->properties->{$propName}->type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $primaryDbProps;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\SchemaDAO', '\SchemaDAO');
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/db/XMLDAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class XMLDAO
|
||||
*
|
||||
* @ingroup db
|
||||
*
|
||||
* @brief Operations for retrieving and modifying objects from an XML data source.
|
||||
*/
|
||||
|
||||
namespace PKP\db;
|
||||
|
||||
use PKP\xml\PKPXMLParser;
|
||||
|
||||
class XMLDAO
|
||||
{
|
||||
/**
|
||||
* Parse an XML file and return data in an object.
|
||||
*
|
||||
* @see PKPXMLParser::parse()
|
||||
*
|
||||
* @param string $file
|
||||
*/
|
||||
public function parse($file)
|
||||
{
|
||||
$parser = new PKPXMLParser();
|
||||
return $parser->parse($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an XML file with the specified handler and return data in an object.
|
||||
*
|
||||
* @see PKPXMLParser::parse()
|
||||
*
|
||||
* @param string $file
|
||||
* @param \PKP\xml\XMLParserDOMHandler $handler reference to the handler to use with the parser.
|
||||
*/
|
||||
public function parseWithHandler($file, $handler)
|
||||
{
|
||||
$parser = new PKPXMLParser();
|
||||
$parser->setHandler($handler);
|
||||
return $parser->parse($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an XML file and return data in an array.
|
||||
*
|
||||
* @see PKPXMLParser::parseStruct()
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $tagsToMatch
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function parseStruct($file, $tagsToMatch = [])
|
||||
{
|
||||
$parser = new PKPXMLParser();
|
||||
return $parser->parseStruct($file, $tagsToMatch);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\db\XMLDAO', '\XMLDAO');
|
||||
}
|
||||
Reference in New Issue
Block a user