Refactor files for better readability

This commit is contained in:
Sebastian Meyer 2024-01-07 21:36:37 +01:00
parent 08a573852e
commit c50f296abb
10 changed files with 267 additions and 159 deletions

View File

@ -152,7 +152,7 @@ class Configuration
if ($violations->count() > 0) { if ($violations->count() > 0) {
throw new ValidationFailedException(null, $violations); throw new ValidationFailedException(null, $violations);
} }
/** @var array<TKey, TValue> $config */ /** @var array<TKey, TValue> */
return $config; return $config;
} }

View File

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace OCC\OaiPmh2\Console; namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\ConsoleCommand;
use OCC\OaiPmh2\Database; use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Database\Format; use OCC\OaiPmh2\Database\Format;
use OCC\OaiPmh2\Database\Record; use OCC\OaiPmh2\Database\Record;
@ -41,7 +42,7 @@ use Symfony\Component\Console\Output\OutputInterface;
name: 'oai:records:add', name: 'oai:records:add',
description: 'Add or update a record in the database' description: 'Add or update a record in the database'
)] )]
class AddRecordCommand extends Command class AddRecordCommand extends ConsoleCommand
{ {
/** /**
* Configures the current command. * Configures the current command.
@ -110,6 +111,8 @@ class AddRecordCommand extends Command
Database::getInstance()->addOrUpdateRecord($record); Database::getInstance()->addOrUpdateRecord($record);
Database::getInstance()->pruneOrphanSets(); Database::getInstance()->pruneOrphanSets();
$this->clearResultCache();
$output->writeln([ $output->writeln([
'', '',
sprintf( sprintf(

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace OCC\OaiPmh2\Console; namespace OCC\OaiPmh2\Console;
use DateTime; use DateTime;
use OCC\OaiPmh2\ConsoleCommand;
use OCC\OaiPmh2\Database; use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Database\Format; use OCC\OaiPmh2\Database\Format;
use OCC\OaiPmh2\Database\Record; use OCC\OaiPmh2\Database\Record;
@ -44,7 +45,7 @@ use Symfony\Component\Console\Output\OutputInterface;
name: 'oai:records:import:csv', name: 'oai:records:import:csv',
description: 'Import records from a CSV file' description: 'Import records from a CSV file'
)] )]
class CsvImportCommand extends Command class CsvImportCommand extends ConsoleCommand
{ {
/** /**
* Configures the current command. * Configures the current command.
@ -99,8 +100,7 @@ class CsvImportCommand extends Command
'noValidation', 'noValidation',
null, null,
InputOption::VALUE_NONE, InputOption::VALUE_NONE,
'Omit content validation (improves performance for large record sets).', 'Omit content validation (improves performance for large record sets).'
false
); );
parent::configure(); parent::configure();
} }
@ -135,7 +135,7 @@ class CsvImportCommand extends Command
} }
$count = 0; $count = 0;
$progressIndicator = new ProgressIndicator($output, 'verbose', 200, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']); $progressIndicator = new ProgressIndicator($output, null, 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']);
$progressIndicator->start('Importing...'); $progressIndicator->start('Importing...');
while ($row = fgetcsv($file)) { while ($row = fgetcsv($file)) {
@ -153,10 +153,10 @@ class CsvImportCommand extends Command
++$count; ++$count;
$progressIndicator->advance(); $progressIndicator->advance();
$progressIndicator->setMessage((string) $count . ' done.'); $progressIndicator->setMessage('Importing... ' . (string) $count . ' records done.');
// Flush to database if memory usage reaches 90% of available limit. // Flush to database if memory usage reaches 30% of available limit.
if ((memory_get_usage() / $memoryLimit) > 0.9) { if ((memory_get_usage() / $memoryLimit) > 0.3) {
Database::getInstance()->flush([Record::class]); Database::getInstance()->flush([Record::class]);
} }
} }
@ -167,6 +167,8 @@ class CsvImportCommand extends Command
fclose($file); fclose($file);
$this->clearResultCache();
$output->writeln([ $output->writeln([
'', '',
sprintf( sprintf(
@ -229,30 +231,6 @@ class CsvImportCommand extends Command
return $columns; return $columns;
} }
/**
* Get the PHP memory limit in bytes.
*
* @return int The memory limit in bytes or -1 if unlimited
*/
protected function getMemoryLimit(): int
{
$ini = trim(ini_get('memory_limit'));
$limit = (int) $ini;
$unit = strtolower($ini[strlen($ini)-1]);
switch($unit) {
case 'g':
$limit *= 1024;
case 'm':
$limit *= 1024;
case 'k':
$limit *= 1024;
}
if ($limit < 0) {
return -1;
}
return $limit;
}
/** /**
* Validate input arguments. * Validate input arguments.
* *

View File

@ -22,7 +22,7 @@ declare(strict_types=1);
namespace OCC\OaiPmh2\Console; namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\Configuration; use OCC\OaiPmh2\ConsoleCommand;
use OCC\OaiPmh2\Database; use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Database\Format; use OCC\OaiPmh2\Database\Format;
use OCC\OaiPmh2\Database\Record; use OCC\OaiPmh2\Database\Record;
@ -42,7 +42,7 @@ use Symfony\Component\Console\Output\OutputInterface;
name: 'oai:records:delete', name: 'oai:records:delete',
description: 'Delete a record from database' description: 'Delete a record from database'
)] )]
class DeleteRecordCommand extends Command class DeleteRecordCommand extends ConsoleCommand
{ {
/** /**
* Configures the current command. * Configures the current command.
@ -89,6 +89,7 @@ class DeleteRecordCommand extends Command
if (isset($record)) { if (isset($record)) {
Database::getInstance()->deleteRecord($record); Database::getInstance()->deleteRecord($record);
$this->clearResultCache();
$output->writeln([ $output->writeln([
'', '',
sprintf( sprintf(

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace OCC\OaiPmh2\Console; namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\Configuration; use OCC\OaiPmh2\Configuration;
use OCC\OaiPmh2\ConsoleCommand;
use OCC\OaiPmh2\Database; use OCC\OaiPmh2\Database;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -40,7 +41,7 @@ use Symfony\Component\Console\Output\OutputInterface;
name: 'oai:records:prune', name: 'oai:records:prune',
description: 'Prune deleted records from database' description: 'Prune deleted records from database'
)] )]
class PruneDeletedRecordsCommand extends Command class PruneDeletedRecordsCommand extends ConsoleCommand
{ {
/** /**
* Configures the current command. * Configures the current command.
@ -75,6 +76,7 @@ class PruneDeletedRecordsCommand extends Command
or ($policy === 'transient' && $forced) or ($policy === 'transient' && $forced)
) { ) {
$deleted = Database::getInstance()->pruneDeletedRecords(); $deleted = Database::getInstance()->pruneDeletedRecords();
$this->clearResultCache();
$output->writeln([ $output->writeln([
'', '',
sprintf( sprintf(

View File

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace OCC\OaiPmh2\Console; namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\ConsoleCommand;
use OCC\OaiPmh2\Database; use OCC\OaiPmh2\Database;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -38,7 +39,7 @@ use Symfony\Component\Console\Output\OutputInterface;
name: 'oai:tokens:prune', name: 'oai:tokens:prune',
description: 'Prune expired resumption tokens from database' description: 'Prune expired resumption tokens from database'
)] )]
class PruneResumptionTokensCommand extends Command class PruneResumptionTokensCommand extends ConsoleCommand
{ {
/** /**
* Executes the current command. * Executes the current command.

View File

@ -23,14 +23,12 @@ declare(strict_types=1);
namespace OCC\OaiPmh2\Console; namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\Configuration; use OCC\OaiPmh2\Configuration;
use OCC\OaiPmh2\ConsoleCommand;
use OCC\OaiPmh2\Database; use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Database\Format; use OCC\OaiPmh2\Database\Format;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException; use Symfony\Component\Validator\Exception\ValidationFailedException;
@ -44,7 +42,7 @@ use Symfony\Component\Validator\Exception\ValidationFailedException;
name: 'oai:formats:update', name: 'oai:formats:update',
description: 'Update metadata formats in database from configuration' description: 'Update metadata formats in database from configuration'
)] )]
class UpdateFormatsCommand extends Command class UpdateFormatsCommand extends ConsoleCommand
{ {
/** /**
* Executes the current command. * Executes the current command.
@ -103,15 +101,7 @@ class UpdateFormatsCommand extends Command
]); ]);
} }
} }
/** @var Application */ $this->clearResultCache();
$app = $this->getApplication();
$app->doRun(
new ArrayInput([
'command' => 'orm:clear-cache:result',
'--flush' => true
]),
new NullOutput()
);
$currentFormats = array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult()); $currentFormats = array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult());
if (count($currentFormats) > 0) { if (count($currentFormats) > 0) {
$output->writeln( $output->writeln(

79
src/ConsoleCommand.php Normal file
View File

@ -0,0 +1,79 @@
<?php
/**
* OAI-PMH 2.0 Data Provider
* Copyright (C) 2023 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace OCC\OaiPmh2;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
/**
* Base class for all OAI-PMH console commands.
*
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
* @package opencultureconsulting/oai-pmh2
*/
abstract class ConsoleCommand extends Command
{
/**
* Clears the result cache.
*
* @return void
*/
protected function clearResultCache(): void
{
/** @var Application */
$app = $this->getApplication();
$app->doRun(
new ArrayInput([
'command' => 'orm:clear-cache:result',
'--flush' => true
]),
new NullOutput()
);
}
/**
* Gets the PHP memory limit in bytes.
*
* @return int The memory limit in bytes or -1 if unlimited
*/
protected function getMemoryLimit(): int
{
$ini = trim(ini_get('memory_limit'));
$limit = (int) $ini;
$unit = strtolower($ini[strlen($ini)-1]);
switch($unit) {
case 'g':
$limit *= 1024;
case 'm':
$limit *= 1024;
case 'k':
$limit *= 1024;
}
if ($limit < 0) {
return -1;
}
return $limit;
}
}

View File

@ -41,7 +41,6 @@ use OCC\OaiPmh2\Database\Set;
use OCC\OaiPmh2\Database\Token; use OCC\OaiPmh2\Database\Token;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter; use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
use Symfony\Component\Filesystem\Path; use Symfony\Component\Filesystem\Path;
use Symfony\Component\Validator\Exception\ValidationFailedException;
/** /**
* Handles all database shenanigans. * Handles all database shenanigans.
@ -124,6 +123,51 @@ class Database
} }
} }
/**
* Delete metadata format and all associated records.
*
* @param Format $format The metadata format
*
* @return void
*/
public function deleteMetadataFormat(Format $format): void
{
$dql = $this->entityManager->createQueryBuilder();
$dql->delete(Record::class, 'record')
->where($dql->expr()->eq('record.format', ':format'))
->setParameter('format', $format->getPrefix());
$query = $dql->getQuery();
$query->execute();
// Explicitly remove associations with sets for deleted records.
$sql = $this->entityManager->getConnection();
$sql->executeStatement("DELETE FROM records_sets WHERE record_format='{$format->getPrefix()}'");
$this->entityManager->remove($format);
$this->entityManager->flush();
$this->pruneOrphanSets();
}
/**
* Delete a record.
*
* @param Record $record The record
*
* @return void
*/
public function deleteRecord(Record $record): void
{
if (Configuration::getInstance()->deletedRecords === 'no') {
$this->entityManager->remove($record);
} else {
$record->setContent(null);
$record->setLastChanged(new DateTime());
}
$this->entityManager->flush();
$this->pruneOrphanSets();
}
/** /**
* Flush all changes to the database. * Flush all changes to the database.
* *
@ -319,7 +363,7 @@ class Database
* *
* @return Result<Sets> The sets and possibly a resumption token * @return Result<Sets> The sets and possibly a resumption token
*/ */
public function getSets($counter = 0): Result public function getSets(int $counter = 0): Result
{ {
$result = []; $result = [];
$maxRecords = Configuration::getInstance()->maxRecords; $maxRecords = Configuration::getInstance()->maxRecords;
@ -388,18 +432,21 @@ class Database
/** /**
* Prune orphan sets. * Prune orphan sets.
* *
* @return void * @return int The number of removed sets
*/ */
public function pruneOrphanSets(): void public function pruneOrphanSets(): int
{ {
$repository = $this->entityManager->getRepository(Set::class); $repository = $this->entityManager->getRepository(Set::class);
$sets = $repository->findAll(); $sets = $repository->findAll();
$count = 0;
foreach ($sets as $set) { foreach ($sets as $set) {
if ($set->isEmpty()) { if ($set->isEmpty()) {
$this->entityManager->remove($set); $this->entityManager->remove($set);
++$count;
} }
} }
$this->entityManager->flush(); $this->entityManager->flush();
return $count;
} }
/** /**
@ -419,45 +466,6 @@ class Database
return count($tokens); return count($tokens);
} }
/**
* Delete metadata format and all associated records.
*
* @param Format $format The metadata format
*
* @return void
*/
public function deleteMetadataFormat(Format $format): void
{
$repository = $this->entityManager->getRepository(Record::class);
$criteria = Criteria::create()->where(Criteria::expr()->eq('format', $format));
$records = $repository->matching($criteria);
foreach ($records as $record) {
$this->entityManager->remove($record);
}
$this->entityManager->remove($format);
$this->entityManager->flush();
$this->pruneOrphanSets();
}
/**
* Delete a record.
*
* @param Record $record The record
*
* @return void
*/
public function deleteRecord(Record $record): void
{
if (Configuration::getInstance()->deletedRecords === 'no') {
$this->entityManager->remove($record);
} else {
$record->setContent(null);
$record->setLastChanged(new DateTime());
}
$this->entityManager->flush();
$this->pruneOrphanSets();
}
/** /**
* This is a singleton class, thus the constructor is private. * This is a singleton class, thus the constructor is private.
* *
@ -480,7 +488,7 @@ class Database
new AttributeDriver([__DIR__ . '/Database']) new AttributeDriver([__DIR__ . '/Database'])
); );
$configuration->setProxyDir(__DIR__ . '/../var/generated'); $configuration->setProxyDir(__DIR__ . '/../var/generated');
$configuration->setProxyNamespace('OCC\OaiPmh2\Proxy'); $configuration->setProxyNamespace('OCC\OaiPmh2\Database\Proxy');
$configuration->setQueryCache( $configuration->setQueryCache(
new PhpFilesAdapter( new PhpFilesAdapter(
'Query', 'Query',

View File

@ -28,6 +28,7 @@ use DOMException;
use DOMNode; use DOMNode;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
/** /**
* An OAI-PMH XML response object. * An OAI-PMH XML response object.
@ -47,6 +48,111 @@ class Document
*/ */
protected DOMElement $rootNode; protected DOMElement $rootNode;
/**
* This holds the current server request.
*/
protected ServerRequestInterface $serverRequest;
/**
* Add XSL processing instructions to XML response document.
*
* @return void
*/
protected function addProcessingInstructions(): void
{
$uri = $this->serverRequest->getUri();
$basePath = $uri->getPath();
if (str_ends_with($basePath, 'index.php')) {
$basePath = pathinfo($basePath, PATHINFO_DIRNAME);
}
$stylesheet = Uri::composeComponents(
$uri->getScheme(),
$uri->getAuthority(),
rtrim($basePath, '/') . '/resources/stylesheet.xsl',
null,
null
);
$xslt = $this->dom->createProcessingInstruction(
'xml-stylesheet',
sprintf(
'type="text/xsl" href="%s"',
$stylesheet
)
);
$this->dom->appendChild($xslt);
}
/**
* Create and append request element.
*
* @return void
*/
protected function appendRequest(): void
{
$uri = $this->serverRequest->getUri();
$baseUrl = Uri::composeComponents(
$uri->getScheme(),
$uri->getAuthority(),
$uri->getPath(),
null,
null
);
$request = $this->dom->createElement('request', $baseUrl);
$this->rootNode->appendChild($request);
foreach ($this->serverRequest->getAttributes() as $param => $value) {
$request->setAttribute(
$param,
htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8')
);
}
}
/**
* Create and append response date element.
*
* @return void
*/
protected function appendResponseDate(): void
{
$responseDate = $this->dom->createElement('responseDate', gmdate('Y-m-d\TH:i:s\Z'));
$this->rootNode->appendChild($responseDate);
}
/**
* Create and append root element.
*
* @return void
*/
protected function appendRootElement(): void
{
$this->rootNode = $this->dom->createElement('OAI-PMH');
$this->rootNode->setAttribute(
'xmlns',
'http://www.openarchives.org/OAI/2.0/'
);
$this->rootNode->setAttribute(
'xmlns:xsi',
'http://www.w3.org/2001/XMLSchema-instance'
);
$this->rootNode->setAttribute(
'xsi:schemaLocation',
'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'
);
$this->dom->appendChild($this->rootNode);
}
/**
* Create the DOM document.
*
* @return void
*/
protected function createDocument(): void
{
$this->dom = new DOMDocument('1.0', 'UTF-8');
$this->dom->preserveWhiteSpace = false;
$this->addProcessingInstructions();
}
/** /**
* Create a new XML element. * Create a new XML element.
* *
@ -101,71 +207,11 @@ class Document
*/ */
public function __construct(ServerRequestInterface $serverRequest) public function __construct(ServerRequestInterface $serverRequest)
{ {
$uri = $serverRequest->getUri(); $this->serverRequest = $serverRequest;
$this->createDocument();
// Create XML document. $this->appendRootElement();
$this->dom = new DOMDocument('1.0', 'UTF-8'); $this->appendResponseDate();
$this->dom->preserveWhiteSpace = false; $this->appendRequest();
// Add processing instructions.
$basePath = $uri->getPath();
if (str_ends_with($basePath, 'index.php')) {
$basePath = pathinfo($basePath, PATHINFO_DIRNAME);
}
$stylesheet = Uri::composeComponents(
$uri->getScheme(),
$uri->getAuthority(),
rtrim($basePath, '/') . '/resources/stylesheet.xsl',
null,
null
);
$xslt = $this->dom->createProcessingInstruction(
'xml-stylesheet',
sprintf(
'type="text/xsl" href="%s"',
$stylesheet
)
);
$this->dom->appendChild($xslt);
// Add root element "OAI-PMH".
$root = $this->dom->createElement('OAI-PMH');
$this->dom->appendChild($root);
$root->setAttribute(
'xmlns',
'http://www.openarchives.org/OAI/2.0/'
);
$root->setAttribute(
'xmlns:xsi',
'http://www.w3.org/2001/XMLSchema-instance'
);
$root->setAttribute(
'xsi:schemaLocation',
'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'
);
// Add element "responseDate".
$responseDate = $this->dom->createElement('responseDate', gmdate('Y-m-d\TH:i:s\Z'));
$root->appendChild($responseDate);
// Add element "request".
$baseUrl = Uri::composeComponents(
$uri->getScheme(),
$uri->getAuthority(),
$uri->getPath(),
null,
null
);
$request = $this->dom->createElement('request', $baseUrl);
$root->appendChild($request);
foreach ($serverRequest->getAttributes() as $param => $value) {
$request->setAttribute(
$param,
htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8')
);
}
$this->rootNode = $root;
} }
/** /**