<?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');
}
|