Initial xentral_oss_20.3.c9ffacf

This commit is contained in:
Alex
2021-05-21 08:49:41 +02:00
parent 5406e4a551
commit 34e5ac43d9
18884 changed files with 2109867 additions and 0 deletions
@@ -0,0 +1,56 @@
<?php
namespace Xentral\Components\Backup\Adapter;
use Xentral\Components\Database\DatabaseConfig;
/**
* Interface AdapterInterface
*
* @package Xentral\Components\Backup\Adapter
*/
interface AdapterInterface
{
/** @var string STATUS_WORKING */
const STATUS_WORKING = 'working';
/** @var string STATUS_WAIT */
const STATUS_WAITING = 'waiting';
/**
* Makes MySQL DUMP
*
* @param DatabaseConfig $config
*
* @param string $file
*
* @param null|string|array $sTable
*
* @param null|string $where
*
* @param bool $quickMode Without SET INNODB_STRICT_MODE=0; Advantage quick and space-saving
*
* @return int PidFile
*/
public function createDump(DatabaseConfig $config, $file, $sTable = null, $where = null, $quickMode=true);
/**
* Makes Backup or System template recovery
*
* @param DatabaseConfig $config
* @param string $file
*
* @return int pidFile
*/
public function restoreDump(DatabaseConfig $config, $file);
/**
* returns the current status
*
* @param string $pidFile
*
* @return string|self::STATUS_WORKING|self::STATUS_WAITING
*/
public function getStatus($pidFile);
}
@@ -0,0 +1,83 @@
<?php
namespace Xentral\Components\Backup\Adapter;
use Xentral\Components\Database\DatabaseConfig;
final class ExecAdapter implements AdapterInterface
{
/** @var DatabaseConfig $config */
private $config;
/** @var int timeout */
const TIME_OUT = 3600;
/**
* @param DatabaseConfig $config
* @param string $file
*
* @param null|string|array $tables
*
* @param null|string $where
*
* @param bool $quickMode Without SET INNODB_STRICT_MODE=0; Advantage quick and space-saving
*
* @return void
*/
public function createDump(DatabaseConfig $config, $file, $tables = null, $where = null, $quickMode = true)
{
$this->config = $config;
$sAsBackup = $this->config->getDatabase();
if ($tables !== null) {
if (is_array($tables)) {
$tables = implode(' ', $tables);
}
$sAsBackup .= ' --tables ' . $tables;
}
if ($where !== null) {
$sAsBackup .= " --where=\"$where\"";
}
if ($quickMode !== true) {
$cmd = "echo 'SET INNODB_STRICT_MODE=0;' > {$file} && mysqldump --no-tablespaces --extended-insert {$sAsBackup} --no-create-db -h{$this->config->getHostname()} -u{$this->config->getUsername()} -p'{$this->config->getPassword()}' >> {$file} && gzip -c {$file} > " . $file . '.gz && rm -f' . $file;
} else {
$cmd = "mysqldump --no-tablespaces --extended-insert {$sAsBackup} --no-create-db -h{$this->config->getHostname()} -u{$this->config->getUsername()} -p'{$this->config->getPassword()}' | gzip > " . $file . '.gz';
}
$this->execute($cmd);
}
/**
* @param DatabaseConfig $config
* @param string $file
*
* @return void
*/
public function restoreDump(DatabaseConfig $config, $file)
{
$this->config = $config;
$cmd = "gunzip < {$file} | mysql -D{$this->config->getDatabase()} -h{$this->config->getHostname()} -u{$this->config->getUsername()} -p'{$this->config->getPassword()}'";
$this->execute($cmd);
}
/**
* @param string $pidFile
*
* @return string
*/
public function getStatus($pidFile)
{
if (file_exists($pidFile) && ($time = file_get_contents($pidFile)) && (time() - (int)$time) < static::TIME_OUT) {
return AdapterInterface::STATUS_WORKING;
}
return AdapterInterface::STATUS_WAITING;
}
/**
* @param string $cmd
*/
protected function execute($cmd)
{
@exec($cmd);
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace Xentral\Components\Backup;
use ApplicationCore;
use Xentral\Components\Backup\Exception\BackupException;
use Xentral\Components\Backup\Adapter\ExecAdapter;
use Xentral\Components\Backup\Logger\BackupLog;
use Xentral\Core\DependencyInjection\ContainerInterface;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices()
{
return [
'DatabaseBackup' => 'onInitDatabaseBackup',
'FileBackup' => 'onInitFileBackup',
'BackupLog' => 'onInitBackupLogger',
];
}
/**
*
* @param ContainerInterface $container
*
* @return DatabaseBackup
*/
public static function onInitDatabaseBackup(ContainerInterface $container)
{
//@codeCoverageIgnoreStart
if (!function_exists('exec')) {
throw new BackupException(sprintf('function "%s" is missing!', 'exec'));
}
//@codeCoverageIgnoreEnd
/** @var ApplicationCore $app */
$app = $container->get('LegacyApplication');
return new DatabaseBackup(new ExecAdapter(), $app->erp->getTMP());
}
/**
* @param ContainerInterface $container
*
* @return FileBackup
*/
public static function onInitFileBackup(ContainerInterface $container)
{
/** @var ApplicationCore $app */
$app = $container->get('LegacyApplication');
return new FileBackup($container->get('BackupLog'), $app->erp->getTMP());
}
public static function onInitBackupLogger(ContainerInterface $container)
{
/** @var ApplicationCore $app */
$app = $container->get('LegacyApplication');
/** @var string $path */
$path = $app->erp->GetRootPath() . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR;
return new BackupLog($path, 'status.txt');
}
}
@@ -0,0 +1,190 @@
<?php
namespace Xentral\Components\Backup;
use Xentral\Components\Backup\Adapter\AdapterInterface;
use Xentral\Components\Database\DatabaseConfig;
use Xentral\Components\Backup\Exception\BackupException;
final class DatabaseBackup
{
/**
* @var AdapterInterface
*/
private $adapter;
/** @var string $tmpPath */
private $tmpPath;
/** @var string lock */
const PID_NAME = 'backup.lock';
/**
* DatabaseBackup constructor.
*
* @param AdapterInterface $adapter
*
* @param string $tmpPath
*/
public function __construct(AdapterInterface $adapter, $tmpPath)
{
$this->adapter = $adapter;
$this->tmpPath = $tmpPath;
}
/**
* Creates MySQL Dump
*
* @param DatabaseConfig $config
* @param string $file
* @param null|string|array $sTable
*
* @param null|string $where
*
* @return void
*/
public function createDump(DatabaseConfig $config, $file, $sTable = null, $where = null)
{
$sPidFile = $this->getLockFile();
file_put_contents($sPidFile, time());
$this->adapter->createDump($config, $file, $sTable, $where);
@unlink($sPidFile);
}
/**
* Restores Database DUMP
*
* @param DatabaseConfig $config
* @param string $file
*
* @return void
*/
public function restoreDump(DatabaseConfig $config, $file)
{
if (!file_exists($file)) {
throw new BackupException(sprintf('Database Dump %s not found!', $file));
}
$sPidFile = $this->getLockFile();
file_put_contents($sPidFile, time());
$this->adapter->restoreDump($config, $file);
@unlink($sPidFile);
}
/**
* @param string $metaFile
*
* @return string|null
*/
public function getMetaInfo($metaFile)
{
if (!empty($metaFile) && file_exists($metaFile) && ($sMetaEnc = file_get_contents($metaFile))) {
return $this->decodeJson(base64_decode($sMetaEnc), true);
}
return null;
}
/**
* @param string $sJSON
* @param bool $bAsHash
*
* @return mixed|null
*/
protected function decodeJson($sJSON, $bAsHash = false)
{
if (($xData = json_decode($sJSON, $bAsHash)) !== null
&& (json_last_error() === JSON_ERROR_NONE)) {
return $xData;
}
return null;
}
/**
*
* @return string|AdapterInterface
*/
public function getLockStatus()
{
return $this->adapter->getStatus($this->getLockFile());
}
/**
* @return string
*/
protected function getLockFile()
{
return rtrim($this->tmpPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . static::PID_NAME;
}
/**
* @param array $tables
* @param array $excludeKeys
*
* @return mixed
*/
public function excludeCheckSumTables($tables, $excludeKeys = [])
{
$default = [
'backup',
'useronline',
'logfile',
'cronjob_starter_running',
'wiki',
'protokoll',
'cronjob_log',
'module_stat',
'checkaltertable',
'konfiguration',
'permissionhistory',
'adapterbox_request_log',
'hook',
'module_action',
'prozessstarter',
'sqlcache',
'systemhealth',
'userkonfiguration',
'artikel',
'shopimport_amazon_throttling',
'lieferschein',
'report_column',
'report_parameter',
'notification_message',
'module_stat_detail',
];
$excludeKeys = array_merge($default, $excludeKeys);
foreach ($excludeKeys as $key) {
if (!array_key_exists($key, $tables)) {
continue;
}
unset($tables[$key]);
}
return $tables;
}
/**
* @param string $backupFile
*
* @return string
*/
public function getMetaFileName($backupFile)
{
$asFile = explode('.', $backupFile);
array_pop($asFile);
$filename = implode('.', $asFile) . '.meta';
return str_replace('.backup', 'backup/snapshots', $filename);
}
/**
* @param string $filePath
*
* @return string|null
*/
public function getDumpMetaData($filePath = null)
{
return $this->getMetaInfo($this->getMetaFileName($filePath));
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Backup\Exception;
use RuntimeException;
class BackupException extends RuntimeException implements BackupExceptionInterface
{
}
@@ -0,0 +1,14 @@
<?php
namespace Xentral\Components\Backup\Exception;
use Xentral\Core\Exception\ComponentExceptionInterface;
/**
* Interface BackupExceptionInterface
*
* @package Xentral\Components\Backup\Exception
*/
interface BackupExceptionInterface extends ComponentExceptionInterface
{
}
@@ -0,0 +1,12 @@
<?php
namespace Xentral\Components\Backup\Exception;
use RuntimeException;
class LogException extends RuntimeException implements BackupExceptionInterface
{
}
+451
View File
@@ -0,0 +1,451 @@
<?php
namespace Xentral\Components\Backup;
use PHPUnit\Runner\Exception;
use Xentral\Components\Backup\Logger\BackupLog;
use Xentral\Components\Backup\Exception\BackupException;
use ZipArchive;
final class FileBackup implements FileBackupInterface
{
/** @var string backup path */
private $sUserPath;
/** @var BackupLog $logger */
private $logger;
/** @var string $cacheTmp */
private $cacheTmp;
/**
* @param BackupLog $logger
* @param string $cacheTmp
*/
public function __construct(BackupLog $logger, $cacheTmp)
{
$this->logger = $logger;
$this->cacheTmp = $cacheTmp;
}
/**
* @return string
*/
protected function getMainPath()
{
$asPath = explode(DIRECTORY_SEPARATOR, $this->sUserPath);
array_pop($asPath);
return implode(DIRECTORY_SEPARATOR, $asPath) . DIRECTORY_SEPARATOR;
}
/**
* returns the full file name path
*
* @param string $filename
* @param bool $bIsSnapshots
* @param null $userPath
*
* @throws BackupException
* @return string
*/
public function getLocalPath($filename, $userPath = null, $bIsSnapshots = true)
{
if (null !== $userPath) {
$this->sUserPath = $userPath;
}
$path = $this->getMainPath();
if ($bIsSnapshots === true) {
$path .= FileBackupInterface::SNAPSHOTS_FOLDER . DIRECTORY_SEPARATOR;
}
if (!file_exists($path) && !@mkdir($path) && !is_dir($path)) {
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $path));
throw new BackupException(sprintf('Directory "%s" was not created', $path));
}
return $path . $filename;
}
/**
* @return string
*/
private function tmpDir()
{
return $this->getMainPath() . 'backup/.backup' . DIRECTORY_SEPARATOR;
}
/**
* @return string
*/
public function getSnapshotsDir()
{
return $this->getMainPath() . FileBackupInterface::SNAPSHOTS_FOLDER . DIRECTORY_SEPARATOR;
}
/**
* @param $path
*
* @return false|int
*/
protected function addLock($path)
{
return $this->logger->write(time(), $path, FileBackupInterface::PID_FILE, false, false);
}
/**
* @return bool
*/
private function tryPurgePidFile()
{
$pidFile = $this->tmpDir() . FileBackupInterface::PID_FILE;
$time = file_get_contents($pidFile);
if ((time() - (int)$time > FileBackupInterface::TIME_OUT)) {
return unlink($pidFile);
}
return false;
}
/**
* @param string|null $userPath
*
* @throws BackupException
* @return string|null
*/
public function begin($userPath = null)
{
if (null !== $userPath) {
$this->sUserPath = $userPath;
}
$path = $this->tmpDir();
if (is_dir($path)) {
@exec('rm -rf ' . $path);
}
$backupDir = $this->getMainPath() . 'backup';
if (file_exists($backupDir . DIRECTORY_SEPARATOR . 'status.txt')) {
@unlink($backupDir . DIRECTORY_SEPARATOR . 'status.txt');
}
if (file_exists($backupDir . DIRECTORY_SEPARATOR . 'session.txt')) {
@unlink($backupDir . DIRECTORY_SEPARATOR . 'session.txt');
}
if (!file_exists($path) && !@mkdir($path, 0777, true) && !is_dir($path)) {
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $path));
throw new BackupException(sprintf('Directory "%s" was not created', $path));
}
if ($this->getLockStatus() === FileBackupInterface::STATUS_WORKING && $this->tryPurgePidFile() === false) {
return null;
//throw new BackupException(sprintf('Backup is Running'));
}
if ($this->addLock($path) === false) {
$this->logger->writePersistent('Failed start backup');
throw new BackupException('Failed start backup');
}
return $path;
}
/**
* @param string $file
*
* @return bool
*/
protected function cleanUp($file)
{
$path = $this->tmpDir();
if ($this->moveDir($path . $file, $this->getLocalPath($file)) === true) {
return $this->deleteDir($path);
}
$this->logger->writePersistent(sprintf('Clean Up of %s failed', $path));
throw new BackupException(sprintf('Clean Up of %s failed', $path));
}
/**
* @param string $class_name
*
* @return bool
*/
protected function classExists($class_name)
{
return class_exists($class_name);
}
/**
* @param ZipArchive $oZip
* @param string $fileName
* @param int $flags
*
* @return mixed
*/
protected function openZipObject($oZip, $fileName, $flags = 0)
{
return $oZip->open($fileName, $flags);
}
/**
* @param string $oldDir
* @param string $newDir
*
* @return bool
*/
protected function moveDir($oldDir, $newDir)
{
return @rename($oldDir, $newDir);
}
/**
* @param string $dir
*
* @return bool
*/
protected function isDir($dir)
{
return @mkdir($dir) || is_dir($dir);
}
/**
* @return string
*/
public function getBackupExtension()
{
return FileBackupInterface::COMPRESS_EXTENSION;
}
/**
* @param string $filename Zipped file name
* @param string $userPath local directory to backup
* @param string|null $sMySQLFile MySQL Backup file
*
* @return bool
*/
public function createBackup($filename, $userPath, $sMySQLFile = null)
{
$this->sUserPath = $userPath;
$rootPath = realpath($userPath);
if (!file_exists($userPath)) {
$this->logger->writePersistent(sprintf('Directory "%s" was not found', $userPath));
throw new BackupException(sprintf('Directory "%s" was not found', $userPath));
}
$tmpFilename = $this->tmpDir() . $filename;
$sMySQLFullPath = $this->tmpDir() . $sMySQLFile;
if (null !== $sMySQLFile && is_file($sMySQLFullPath) && filesize($sMySQLFullPath) > 1024) {
$this->logger->write('Add MySQL file to Zip');
exec('cd ' . $rootPath . ' && mv ' . $sMySQLFullPath . ' ' . $sMySQLFile);
}
exec('cd ' . $rootPath . ' && zip -r -9 ' . $tmpFilename . ' * .[^.]* -x "wiki/*"');
if (null !== $sMySQLFile) {
exec('cd ' . $rootPath . ' && rm -f ' . $sMySQLFile);
}
return $this->cleanUp($filename);
}
/**
* @param string $dirPath
*
* @return bool
*/
private function deleteDir($dirPath)
{
if (is_dir($dirPath)) {
if (substr($dirPath, strlen($dirPath) - 1, 1) !== '/') {
$dirPath .= '/';
}
$files = glob($dirPath . '*', GLOB_MARK);
foreach ($files as $file) {
if (is_dir($file)) {
$this->deleteDir($file);
} else {
unlink($file);
}
}
return rmdir($dirPath);
}
$this->logger->writePersistent(sprintf('Deleted DIR %s failed', $dirPath));
throw new BackupException(sprintf('Deleted DIR %s failed', $dirPath));
}
/**
* @param string $backupFile
* @param string $userPath
* @param array $options
*
* @return bool
*/
public function restoreFileSystem($backupFile, $userPath, $options = [])
{
$default = ['template_file_dir' => null, 'exclude_dir' => ['wiki']];
$options = array_merge($default, $options);
$templateFileDir = $options['template_file_dir'];
$this->sUserPath = $userPath;
$bIsSnapshots = null === $templateFileDir;
$this->sUserPath = null === $templateFileDir ? $userPath : $templateFileDir;
if (file_exists($file = $this->getLocalPath($backupFile, null, $bIsSnapshots))) {
$userDataPath = realpath($userPath);
$tmpExtract = $this->tmpDir() . FileBackupInterface::LOCAL_FILES_DIR_NAME . 'tmp';
if (!file_exists($tmpExtract) && !@mkdir($tmpExtract) && !is_dir($tmpExtract)) {
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $tmpExtract));
throw new BackupException(sprintf('Directory "%s" was not created', $tmpExtract));
}
if (!$this->classExists('ZipArchive')) {
$this->logger->writePersistent('Class ZipArchive is missing!');
throw new BackupException('Class ZipArchive is missing!');
}
$oZip = new ZipArchive();
if ($this->openZipObject($oZip, $file, ZipArchive::CHECKCONS) !== true) {
$this->logger->writePersistent(sprintf('Failure to open file in "%s"', $file));
throw new BackupException(sprintf('Failure to open file in "%s"', $file));
}
$oZip->extractTo($tmpExtract);
$oZip->close();
// move user data
$shortTmp = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.rmTmp';
$sBeforeTmp = $shortTmp . uniqid('', true) . 'before';
if (!$this->isDir($sBeforeTmp)) {
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $sBeforeTmp));
throw new BackupException(sprintf('Directory "%s" was not created', $sBeforeTmp));
}
$this->logger->write('Moving userdata away');
if (!$this->moveDir($userDataPath, $sBeforeTmp)) {
$this->logger->writePersistent(sprintf('Moving %s into %s failed! ', $userDataPath, $sBeforeTmp));
throw new BackupException(sprintf('Moving %s into %s failed! ', $userDataPath, $sBeforeTmp));
}
if (array_key_exists('exclude_dir', $options) && is_array($options['exclude_dir'])) {
$this->excludeDirectory($options['exclude_dir'], $tmpExtract, $sBeforeTmp);
}
$this->logger->write('Recovering userData');
if (!$this->moveDir($tmpExtract, $userDataPath)) {
$this->logger->writePersistent(sprintf('Moving %s into %s failed!', $tmpExtract, $userDataPath));
throw new BackupException(sprintf('Moving %s into %s failed!', $tmpExtract, $userDataPath));
}
// FIX TMP ISSUE
if (!empty($this->cacheTmp) && is_dir($this->cacheTmp)) {
$this->logger->write('Delete DB tmp');
$this->deleteDir($this->cacheTmp);
}
// remove DB if exists
$backupFileExploded = explode('.', $backupFile);
array_pop($backupFileExploded);
$tmpSql = implode('.', $backupFileExploded) . '.sql.gz';
if (is_file($userDataPath . DIRECTORY_SEPARATOR . $tmpSql)) {
exec('cd ' . $userDataPath . ' && rm -f ' . $tmpSql);
}
return $this->deleteDir($this->tmpDir()) && $this->deleteDir($sBeforeTmp);
}
return false;
}
/**
* @param array $excludeDir
* @param string $tmpDir extracted temporally directory
* @param string $oldUserDataDir
*/
protected function excludeDirectory($excludeDir = [], $tmpDir, $oldUserDataDir)
{
foreach ($excludeDir as $directory) {
// EXCLUDE WIKI DIRECTORY
$keepPath = $tmpDir . DIRECTORY_SEPARATOR . $directory;
$keepDirTmp = $oldUserDataDir . DIRECTORY_SEPARATOR . $directory;
if (!is_dir($keepPath) && $directory !== 'wiki') {
$this->logger->writePersistent(sprintf('Directory "%s" cannot be skipped', $keepPath));
throw new BackupException(sprintf('Directory "%s" cannot be skipped', $keepPath));
}
if ($directory !== 'wiki') {
$this->deleteDir($keepPath);
}
if (file_exists($keepDirTmp)) {
$oldDirTmp = rtrim(
sys_get_temp_dir(),
DIRECTORY_SEPARATOR
) . DIRECTORY_SEPARATOR . '.' . $directory . 'Tmp' . uniqid('', true);
if (!$this->isDir($oldDirTmp)) {
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $oldDirTmp));
throw new BackupException(sprintf('Directory "%s" was not created', $oldDirTmp));
}
$oldDir = $oldDirTmp . DIRECTORY_SEPARATOR . $directory;
if (!$this->moveDir($keepDirTmp, $oldDir)) {
$this->logger->writePersistent(sprintf('Could not move %s directory into "%s"', $directory,
$oldDir));
throw new BackupException(sprintf('Could not move %s directory into "%s"', $directory,
$oldDir));
}
}
// Reset Latest WIKI Directory
if (isset($oldDir) && is_dir($oldDir)) {
$this->logger->write('Reset Wiki Directory');
if (!$this->moveDir($oldDir, $keepPath)) {
$this->logger->writePersistent(sprintf('Could not move %s directory into "%s"', $oldDir,
$keepPath));
throw new BackupException(sprintf('Could not move %s directory into "%s"', $oldDir, $keepPath));
}
}
}
}
/**
* @param string|null $userDataDir
*
* @return string
*/
public function getLockStatus($userDataDir = null)
{
if (null !== $userDataDir) {
$this->sUserPath = $userDataDir;
}
$path = $this->tmpDir();
if (file_exists($path . FileBackupInterface::PID_FILE) &&
($time = file_get_contents($path . FileBackupInterface::PID_FILE)) &&
(time() - (int)$time < FileBackupInterface::TIME_OUT)
) {
return FileBackupInterface::STATUS_WORKING;
}
return FileBackupInterface::STATUS_WAITING;
}
/**
* Clean everything without files move. This might be used, when breaking started backup job
*
* @return bool
*/
public function breakCleanUp()
{
return $this->deleteDir($this->tmpDir());
}
}
@@ -0,0 +1,56 @@
<?php
namespace Xentral\Components\Backup;
use Xentral\Components\Backup\Exception\BackupException;
interface FileBackupInterface
{
/** @var string STATUS_WAIT */
const STATUS_WAITING = 'waiting';
/** @var string STATUS_WORKING */
const STATUS_WORKING = 'working';
/** @var string Extension for the whole backup */
const COMPRESS_EXTENSION = 'zip';
/** @var string pid file */
const PID_FILE = 'backup.lock';
/** @var int Timeout */
const TIME_OUT = 3600;
/** @var string snapshots folder */
const SNAPSHOTS_FOLDER = 'backup/snapshots';
/** @var string user data directory */
const LOCAL_FILES_DIR_NAME = 'userdata';
/**
* @param string|null $userPath
*
* @throws BackupException
* @return string|null
*/
public function begin($userPath = null);
/**
* @param string $filename
* @param string $userPath
* @param string|null $sMySQLFile
*
* @return bool
*/
public function createBackup($filename, $userPath, $sMySQLFile = null);
/**
* @param string $backupFile
* @param string $userPath
* @param array $options
*
* @return bool
*/
public function restoreFileSystem($backupFile, $userPath, $options = []);
/**
* @return string
*/
public function getLockStatus();
}
@@ -0,0 +1,172 @@
<?php
namespace Xentral\Components\Backup\Logger;
use Xentral\Components\Backup\Exception\LogException;
final class BackupLog
{
/** @var string|null $path */
private $fullPath;
/** @var string|null $storagePath */
private $storagePath;
/** @var string|null $fileName */
private $fileName;
/** @var string $persistentFile */
private static $persistentFile = 'backup_logger.txt';
public function __construct($path = null, $fileName = null)
{
if (null !== $path && null !== $fileName) {
$this->fullPath = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
}
$this->storagePath = $path;
$this->fileName = $fileName;
}
/**
* @param string $message
*
* @param string|null $path
* @param string|null $fileName
*
* @param bool $withDate
*
* @param bool $append
*
* @return false|int
*/
public function write($message, $path = null, $fileName = null, $withDate = true, $append = true)
{
$flag = FILE_APPEND | LOCK_EX;
$path = $this->getFullPath($path, $fileName);
if (null === $path || (!file_exists($path) && !@touch($path))) {
throw new LogException(sprintf('cannot access or create file %s', $path));
}
$message = $withDate === true ? time() . ': ' . $message : $message;
if ($append === false) {
$flag &= ~FILE_APPEND;
}
return file_put_contents($path, $message . "\n", $flag);
}
/**
* @param int $linePosition
*
* @param string|null $path
*
* @param string|null $fileName
*
* @return mixed|string
*/
public function tail($linePosition = 0, $path = null, $fileName = null)
{
$path = $this->getFullPath($path, $fileName);
if (null === $path || !file_exists($path)) {
throw new LogException(sprintf('File %s cannot be found!', $path));
}
$output = '';
if (($xData = file($path, FILE_SKIP_EMPTY_LINES)) && count($xData) > 0) {
$key = (int)$linePosition === 0 ? count($xData) - 1 : $linePosition;
if (!array_key_exists($key, $xData)) {
throw new LogException(sprintf('Offset %d is missing', (int)$linePosition));
}
$output = $xData[$key];
}
return $output;
}
/**
* @param string|null $path
*
* @param string|null $fileName
*
* @return bool|false|string
*/
public function getContent($path = null, $fileName = null)
{
$path = $this->getFullPath($path, $fileName);
if (null === $path || !file_exists($path)) {
throw new LogException(sprintf('File %s cannot be found!', $path));
}
return file_get_contents($path);
}
/**
* @param null $path
*
* @param string|null $fileName
*
* @throws LogException
* @return bool
*/
public function delete($path = null, $fileName = null)
{
$path = $this->getFullPath($path, $fileName);
if (null !== $path && !file_exists($path)) {
return false;
//throw new LogException(sprintf('File %s cannot be deleted!', $path));
}
return unlink($path);
}
/**
* @param null $path
* @param null $fileName
*
* @return string|null
*/
private function getFullPath($path = null, $fileName = null)
{
if ($path === null && $fileName === null) {
return $this->fullPath;
}
if ($path !== null && $fileName !== null) {
return rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
}
if ($fileName !== null && $path === null && $this->storagePath !== null) {
return rtrim($this->storagePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
}
if ($path !== null && $fileName === null && $this->fileName !== null) {
return rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->fileName;
}
return null;
}
/**
* @param string $message
*
* @return void
*/
public function writePersistent($message)
{
$this->write($message, null, self::$persistentFile, true, false);
}
/**
* @return string
*/
public function getPersistentFileName()
{
return self::$persistentFile;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Xentral\Components\Barcode;
use Xentral\Components\Barcode\Exception\InvalidArgumentException;
final class BarcodeFactory
{
/**
* @param string $codeText
* @param string $ecLevel Error correction level [L|M|Q|H]
*
* @return Qrcode
*/
public function createQrCode($codeText, $ecLevel = 'L')
{
$codeType = 'QRCODE,' . $ecLevel;
if (!in_array($codeType, Qrcode::$validTypes, true)) {
throw new InvalidArgumentException('Invalid error correction level: ' . $ecLevel);
}
$barcode2d = new TcpdfBarcode2d($codeType, $codeText);
return new Qrcode($barcode2d);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Xentral\Components\Barcode;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices()
{
return [
'BarcodeFactory' => 'onInitBarcodeFactory',
];
}
/**
* @return BarcodeFactory
*/
public static function onInitBarcodeFactory()
{
return new BarcodeFactory();
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Barcode\Exception;
use RuntimeException;
class BarcodeCreationFailedException extends RuntimeException implements BarcodeExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Barcode\Exception;
use Xentral\Core\Exception\ComponentExceptionInterface;
interface BarcodeExceptionInterface extends ComponentExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Barcode\Exception;
use InvalidArgumentException as SplInvalidArgumentException;
class InvalidArgumentException extends SplInvalidArgumentException implements BarcodeExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Barcode\Exception;
use RuntimeException;
class MissingPhpExtensionException extends RuntimeException implements BarcodeExceptionInterface
{
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace Xentral\Components\Barcode;
final class Qrcode
{
/** @var string TYPE_QRCODE */
const TYPE_DEFAULT = 'QRCODE';
/** @var string TYPE_EC_LOW Low error correction */
const TYPE_EC_LOW = 'QRCODE,L';
/** @var string TYPE_EC_MEDIUM Medium error correction */
const TYPE_EC_MEDIUM = 'QRCODE,M';
/** @var string TYPE_EC_QUARTILE Better error correction */
const TYPE_EC_QUARTILE = 'QRCODE,Q';
/** @var string TYPE_EC_HIGH Best error correction */
const TYPE_EC_HIGH = 'QRCODE,H';
/** @var array $validTypes */
public static $validTypes = [
self::TYPE_DEFAULT,
self::TYPE_EC_LOW,
self::TYPE_EC_MEDIUM,
self::TYPE_EC_QUARTILE,
self::TYPE_EC_HIGH,
];
/** @var TcpdfBarcode2d $barcode */
private $barcode;
/**
* @param TcpdfBarcode2d $barcode
*/
public function __construct(TcpdfBarcode2d $barcode)
{
$this->barcode = $barcode;
}
/**
* @return string
*/
public function getText()
{
return $this->barcode->getText();
}
/**
* Returns the QR code as array representation
*
* @return array
*/
public function toArray()
{
return $this->barcode->getBarcodeArray();
}
/**
* Returns the QR code as HTML representation
*
* @param int $width Width of a single rectangle element in pixels.
* @param int $height Height of a single rectangle element in pixels.
* @param string $color Foreground color for bar elements (background is transparent).
*
* @return string HTML code
*/
public function toHtml($width = 10, $height = 10, $color = 'black')
{
return $this->barcode->getBarcodeHtml($width, $height, $color);
}
/**
* Returns the QR code as SVG document
*
* @param int $width Width of a single rectangle element in user units
* @param int $height Height of a single rectangle element in user units
* @param string $color Foreground color (in SVG format) for bar elements (background is transparent)
*
* @return string SVG document
*/
public function toSvg($width = 10, $height = 10, $color = 'black')
{
return $this->barcode->getBarcodeSvg($width, $height, $color);
}
/**
* Returns the QR code as PNG image (requires GD or Imagick library)
*
* @param int $width Width of a single rectangle element in pixels
* @param int $height Height of a single rectangle element in pixels
* @param array $color RGB-Array (0-255) foreground color for bar elements (background is transparent)
*
* @return string Image as string
*/
public function toPng($width = 10, $height = 10, $color = [0, 0, 0])
{
return $this->barcode->getBarcodePng($width, $height, $color);
}
}
@@ -0,0 +1,148 @@
<?php
namespace Xentral\Components\Barcode;
use TCPDF2DBarcode;
use Xentral\Components\Barcode\Exception\BarcodeCreationFailedException;
use Xentral\Components\Barcode\Exception\InvalidArgumentException;
use Xentral\Components\Barcode\Exception\MissingPhpExtensionException;
/**
* Anti-Corruption-Layer for TCPDF2DBarcode class
*/
final class TcpdfBarcode2d
{
/** @var array $validTypes */
public static $validTypes = [
Qrcode::TYPE_DEFAULT,
Qrcode::TYPE_EC_LOW,
Qrcode::TYPE_EC_MEDIUM,
Qrcode::TYPE_EC_QUARTILE,
Qrcode::TYPE_EC_HIGH,
];
/** @var TCPDF2DBarcode $barcode */
private $barcode;
/** @var string $codeType */
private $type;
/** @var string $codeText */
private $text;
/**
* @param string $type
* @param string $text
*
* @throws InvalidArgumentException
* @throws BarcodeCreationFailedException
*/
public function __construct($type, $text)
{
if (empty($type)) {
throw new InvalidArgumentException('Could not create barcode. Required parameter "type" is empty.');
}
if (empty($text)) {
throw new InvalidArgumentException('Could not create barcode. Required parameter "text" is empty.');
}
if (!in_array($type, self::$validTypes, true)) {
throw new InvalidArgumentException(sprintf(
'Could not create barcode. Invalid Type: "%s". Valid types: [%s]',
$type,
implode('|', self::$validTypes)
));
}
$barcode = new TCPDF2DBarcode($text, $type);
if ($barcode->getBarcodeArray() === false) {
throw new BarcodeCreationFailedException(sprintf(
'Could not create barcode. Type "%s" - Text "%s"', $type, $text
));
}
$this->barcode = $barcode;
$this->text = $text;
$this->type = $type;
}
/**
* @return string
*/
public function getText()
{
return $this->text;
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @return array
*/
public function getBarcodeArray()
{
return $this->barcode->getBarcodeArray();
}
/**
* Returns the QR code as HTML representation
*
* @param int $width Width of a single rectangle element in pixels.
* @param int $height Height of a single rectangle element in pixels.
* @param string $color Foreground color for bar elements (background is transparent).
*
* @return string HTML code
*/
public function getBarcodeHtml($width = 10, $height = 10, $color = 'black')
{
return $this->barcode->getBarcodeHTML($width, $height, $color);
}
/**
* Returns the QR code as SVG document
*
* @param int $width Width of a single rectangle element in user units
* @param int $height Height of a single rectangle element in user units
* @param string $color Foreground color (in SVG format) for bar elements (background is transparent)
*
* @return string SVG document
*/
public function getBarcodeSvg($width = 10, $height = 10, $color = 'black')
{
return $this->barcode->getBarcodeSVGcode($width, $height, $color);
}
/**
* Returns the QR code as PNG image (requires GD or Imagick library)
*
* @param int $width Width of a single rectangle element in pixels
* @param int $height Height of a single rectangle element in pixels
* @param array $color RGB-Array (0-255) foreground color for bar elements (background is transparent)
*
* @throws MissingPhpExtensionException If gd and imagick extension missing
* @throws BarcodeCreationFailedException
*
* @return string Image as string
*/
public function getBarcodePng($width, $height, $color = [0, 0, 0])
{
$imageData = $this->barcode->getBarcodePngData($width, $height, $color);
if (!function_exists('imagecreate') && !extension_loaded('imagick')) {
throw new MissingPhpExtensionException(
'Barcode image creation failed. PHP extension "gd" or "imagick" is required; both missing.'
);
}
if ($imageData === false) {
throw new BarcodeCreationFailedException(
'Barcode image creation failed. Unknown error.'
);
}
return $imageData;
}
}
+25
View File
@@ -0,0 +1,25 @@
# Barcodes
## QR-Codes
### Beispiel
```php
$factory = $this->app->Container->get('BarcodeFactory');
// Qrcode-Objekt erzeugen
$ecLevel = 'M'; // M = Medium error correction
$qrcode = $factory->createQrCode($qrtext, $ecLevel);
// Varianten für die Ausgabe
$html = $qrcode->toHtml($width, $height, $color);
$svg = $qrcode->toSvg($width, $height, $color);
$png = $qrcode->toPng($width, $height, $color);
```
##### Fehlerkorrektur-Levels
* `L` = Low / Niedrige Fehlerkorrektur (Default)
* `M` = Medium / Mittlere Fehlerkorrektur
* `Q` = Quartile / Bessere Fehlerkorrektur
* `H` = High / Höchste Fehlerkorrektur
@@ -0,0 +1,197 @@
<?php
namespace Xentral\Components\Database\Adapter;
use Generator;
interface AdapterInterface
{
/**
* @return void
*/
public function connect();
/**
* @return void
*/
public function disconnect();
/**
* @return bool
*/
public function inTransaction();
/**
* @return void
*/
public function beginTransaction();
/**
* @return void
*/
public function rollback();
/**
* @return void
*/
public function commit();
/**
* @return int
*/
public function lastInsertId();
/**
* @param array $values
* @param string $statement
*
* @return void
*/
public function perform($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return int
*/
public function fetchAffected($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchAll($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchAssoc($statement, array $values = []);
/**
* @param string $statement
* @param array $values
* @param bool $includeGroupColumn
*
* @return array
*/
public function fetchGroup($statement, array $values = [], $includeGroupColumn = false);
/**
* @param string $statement
* @param array $values
*
* @return int|float|string|false false on empty result
*/
public function fetchValue($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchRow($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchCol($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchPairs($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return Generator
*/
public function yieldCol($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return Generator
*/
public function yieldAssoc($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return Generator
*/
public function yieldAll($statement, array $values = []);
/**
* @param string $statement
* @param array $values
*
* @return Generator
*/
public function yieldPairs($statement, array $values = []);
/**
* Escapes values for "BOOLEAN" and (TINY)INT columns
*
* @param bool|null $value
* @param bool $isNullable
*
* @return string
*/
public function escapeBool($value, $isNullable = false);
/**
* Escapes values for INT columns
*
* @param mixed $value
* @param bool $isNullable
*
* @return string
*/
public function escapeInt($value, $isNullable = false);
/**
* Escapes values for FLOAT, DOUBLE and REAL columns
*
* @param mixed $value
* @param bool $isNullable
*
* @return string
*/
public function escapeDecimal($value, $isNullable = false);
/**
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
*
* @param mixed $value
* @param bool $isNullable
*
* @return string
*/
public function escapeString($value, $isNullable = false);
/**
* Escapes and quotes an identifier (column or table name)
*
* @param string $value
*
* @return string
*/
public function escapeIdentifier($value);
}
@@ -0,0 +1,883 @@
<?php
namespace Xentral\Components\Database\Adapter;
use Generator;
use mysqli;
use mysqli_result;
use mysqli_stmt;
use Xentral\Components\Database\DatabaseConfig;
use Xentral\Components\Database\Exception\BindParameterException;
use Xentral\Components\Database\Exception\ConnectionException;
use Xentral\Components\Database\Exception\EscapingException;
use Xentral\Components\Database\Exception\QueryFailureException;
use Xentral\Components\Database\Exception\TransactionException;
use Xentral\Components\Database\Parser\MysqliArrayValueParser;
use Xentral\Components\Database\Parser\MysqliNamedParameterParser;
use Xentral\Components\Database\Profiler\ProfilerInterface;
final class MysqliAdapter implements AdapterInterface
{
/** @var mysqli|null $connection */
private $connection;
/** @var DatabaseConfig $config */
private $config;
/** @var bool $transactionActive */
private $transactionActive = false;
/** @var int|null $reconnectCounter */
private $reconnectCounter;
/** @var int $reconnectLimit */
private $reconnectMaxCount = 5;
/** @var ProfilerInterface|null $profiler */
private $profiler;
/**
* @param DatabaseConfig $config
* @param ProfilerInterface|null $profiler
*/
public function __construct(DatabaseConfig $config, ProfilerInterface $profiler = null)
{
$this->config = $config;
$this->profiler = $profiler;
}
/**
* @throws ConnectionException
*
* @return void
*/
public function connect()
{
if ($this->connection !== null) {
return;
}
if ($this->reconnectCounter === null) {
$this->reconnectCounter = 0;
} else {
$this->reconnectCounter++;
}
if ($this->reconnectCounter >= $this->reconnectMaxCount) {
throw new ConnectionException(sprintf(
'Too many reconnects. Reconnect count: %d (Max allowed %d)',
$this->reconnectCounter,
$this->reconnectMaxCount
));
}
$this->startProfiler(__FUNCTION__);
$connection = new mysqli(
$this->config->getHostname(),
$this->config->getUsername(),
$this->config->getPassword(),
null,
$this->config->getPort()
);
if ($connection->connect_errno > 0) {
throw new ConnectionException(sprintf(
'Database connection to host "%s" failed. Error code #%s. Error message: %s',
$this->config->getHostname(),
$connection->connect_errno,
$connection->connect_error
));
}
$connection->select_db($this->config->getDatabase());
if ($connection->errno > 0) {
throw new ConnectionException(sprintf(
'Database selection failed for database "%s". Error code #%s. Error message: %s',
$this->config->getDatabase(),
$connection->errno,
$connection->error
));
}
// @see https://www.php.net/manual/de/mysqlinfo.concepts.charset.php
if (!$connection->set_charset($this->config->getCharset())) {
throw new ConnectionException(sprintf(
'Failed to set character set "%s". Error: %s',
$this->config->getCharset(),
$connection->error
));
}
if (!$connection->autocommit(true)) {
throw new ConnectionException(sprintf(
'Failed to activate auto commit. Error: %s',
$connection->error
));
}
$this->finishProfiler(null, [
'dbname' => $this->config->getDatabase(),
'host' => $this->config->getHostname(),
'port' => $this->config->getPort(),
]);
$this->connection = $connection;
foreach ($this->config->getQueries() as $query) {
$this->perform($query);
}
}
/**
* @return void
*/
public function disconnect()
{
if ($this->connection === null) {
return;
}
$this->startProfiler(__FUNCTION__);
$this->connection->close();
$this->connection = null;
$this->finishProfiler();
}
/**
* @return bool
*/
public function inTransaction()
{
return $this->transactionActive;
}
/**
* @throws TransactionException If transaction is already started
*
* @return void
*/
public function beginTransaction()
{
if ($this->inTransaction()) {
throw new TransactionException('Transaction is already started.');
}
$this->connect();
if ($this->connection->begin_transaction() === false) {
throw new TransactionException(sprintf('Transaction start failed: %s', $this->connection->error));
}
$this->transactionActive = true;
}
/**
* @throws TransactionException
*
* @return void
*/
public function commit()
{
if (!$this->inTransaction()) {
throw new TransactionException('Transaction not started.');
}
$this->connection->commit();
$this->connection->autocommit(true);
$this->transactionActive = false;
}
/**
* @throws TransactionException
*
* @return void
*/
public function rollback()
{
if (!$this->inTransaction()) {
throw new TransactionException('Transaction not started.');
}
$this->connection->rollback();
$this->connection->autocommit(true);
$this->transactionActive = false;
}
/**
* @return int
*/
public function lastInsertId()
{
return (int)$this->connection->insert_id;
}
/**
* @param array $values
* @param string $statement
*
* @throws QueryFailureException
*
* @return void
*/
public function perform($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$query = $this->getMysqliStatement($statement, $values);
if (!is_object($query)) {
throw new QueryFailureException(sprintf('Database query failed: %s', $this->connection->error));
}
$query->close();
$this->finishProfiler($statement, $values);
}
/**
* @param string $statement
* @param array $values
*
* @throws QueryFailureException
*
* @return int
*/
public function fetchAffected($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$query = $this->getMysqliStatement($statement, $values);
if (!is_object($query)) {
throw new QueryFailureException(sprintf('Database query failed: %s', $this->connection->error));
}
$affectedRows = (int)$query->affected_rows;
$query->close();
$this->finishProfiler($statement, $values);
return $affectedRows;
}
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchAll($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$data = [];
while ($row = $result->fetch_assoc()) {
$data[] = $row;
}
$result->close();
$this->finishProfiler($statement, $values);
return $data;
}
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchAssoc($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$data = [];
while ($row = $result->fetch_assoc()) {
$assocKey = reset($row); // Fetch first array value
$data[$assocKey] = $row;
}
$result->close();
$this->finishProfiler($statement, $values);
return $data;
}
/**
* @param string $statement
* @param array $values
*
* @return array Empty array on empty result
*/
public function fetchRow($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
if ($result->num_rows === 0) {
$result->close();
return [];
}
$data = $result->fetch_assoc();
$result->close();
$this->finishProfiler($statement, $values);
return $data;
}
/**
* @param string $statement
* @param array $values
*
* @return int|float|string|false false on empty result
*/
public function fetchValue($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
if ($result->num_rows === 0) {
$result->close();
return false;
}
$data = $result->fetch_assoc();
$result->close();
$this->finishProfiler($statement, $values);
return reset($data);
}
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function fetchCol($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$data = [];
while ($row = $result->fetch_assoc()) {
$firstValue = reset($row); // Fetch first array value
$data[] = $firstValue;
}
$result->close();
$this->finishProfiler($statement, $values);
return $data;
}
/**
* @param string $statement
* @param array $values
*
* @throws QueryFailureException
*
* @return array
*/
public function fetchPairs($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
if ($result->field_count !== 2) {
throw new QueryFailureException('Field count does not match. fetchPairs() allows only two fields.');
}
$data = [];
while ($row = $result->fetch_assoc()) {
$key = array_shift($row);
$value = array_shift($row);
$data[$key] = $value;
}
$result->close();
$this->finishProfiler($statement, $values);
return $data;
}
/**
* @param string $statement
* @param array $values
* @param bool $includeGroupColumn
*
* @return array
*/
public function fetchGroup($statement, array $values = [], $includeGroupColumn = false)
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$data = [];
$includeGroupColumn = (bool)$includeGroupColumn;
while ($row = $result->fetch_assoc()) {
$group = $includeGroupColumn === true ? reset($row) : array_shift($row); // Fetch first array value
if (!isset($data[$group])) {
$data[$group] = [];
}
$data[$group][] = $row;
}
$result->close();
$this->finishProfiler($statement, $values);
return $data;
}
/**
* @param string $statement
* @param array $values
*
* @return Generator
*/
public function yieldAll($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$this->finishProfiler($statement, $values);
while ($row = $result->fetch_assoc()) {
yield $row;
}
$result->close();
}
/**
* @param string $statement
* @param array $values
*
* @return Generator
*/
public function yieldAssoc($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$this->finishProfiler($statement, $values);
while ($row = $result->fetch_assoc()) {
$assocKey = reset($row); // Fetch first array value
yield $assocKey => $row;
}
$result->close();
}
/**
* @param string $statement
* @param array $values
*
* @return Generator
*/
public function yieldCol($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$this->finishProfiler($statement, $values);
while ($row = $result->fetch_assoc()) {
$firstValue = reset($row); // Fetch first array value
yield $firstValue;
}
$result->close();
}
/**
* @param string $statement
* @param array $values
*
* @throws QueryFailureException
*
* @return Generator
*/
public function yieldPairs($statement, array $values = [])
{
$this->connect();
$this->startProfiler(__FUNCTION__);
$result = $this->getMysqliResult($statement, $values);
$this->finishProfiler($statement, $values);
if ($result->field_count !== 2) {
throw new QueryFailureException('Field count does not match. yieldPairs() allows only two fields.');
}
while ($row = $result->fetch_assoc()) {
$key = array_shift($row);
$value = array_shift($row);
yield $key => $value;
}
$result->close();
}
/**
* Escapes values for "BOOLEAN" and (TINY)INT columns
*
* @param bool|null $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeBool($value, $isNullable = false)
{
if ($isNullable === true && $value === null) {
return 'NULL';
}
if (!is_bool($value)) {
throw new EscapingException('Can not escape bool. Value is not a bool.');
}
return $value === true ? '1' : '0';
}
/**
* Escapes values for INT columns
*
* @param mixed $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeInt($value, $isNullable = false)
{
if ($isNullable === true && $value === null) {
return 'NULL';
}
if (!is_int($value)) {
throw new EscapingException('Can not escape integer. Value is not an integer.');
}
return (string)(int)$value;
}
/**
* Escapes values for FLOAT, DOUBLE and REAL columns
*
* @param mixed $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeDecimal($value, $isNullable = false)
{
if ($isNullable === true && $value === null) {
return 'NULL';
}
if (!is_numeric($value)) {
throw new EscapingException('Can not escape decimal. Value is not numeric.');
}
return (string)$value;
}
/**
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
*
* @param mixed $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeString($value, $isNullable = false)
{
if ($isNullable === true && $value === null) {
return 'NULL';
}
if (!is_string($value)) {
throw new EscapingException('Can not escape string. Value is not a string.');
}
$this->connect();
return "'" . $this->connection->real_escape_string($value) . "'";
}
/**
* Escapes and quotes an identifier (column or table name)
*
* @param string $value
*
* @throws EscapingException
*
* @return string
*/
public function escapeIdentifier($value)
{
if (!is_string($value)) {
throw new EscapingException('Can not escape identifier. Passed value is not a string.');
}
if (empty(trim($value))) {
throw new EscapingException('Can not escape identifier. Passed value is empty.');
}
$parts = explode('.', $value);
if (count($parts) > 2) {
throw new EscapingException('Can not escape identifier. Identifier contains more than one dots.');
}
$partsCleaned = [];
foreach ($parts as $part) {
if (empty(trim($part))) {
throw new EscapingException(
'Can not escape identifier. Parts before and after the dot can not be empty.'
);
}
if (strlen($part) > 64) {
throw new EscapingException(
'Can not escape identifier. Identifier is too long. Only 64 characters are allowed.'
);
}
$partCleaned = preg_replace('/[^A-Za-z0-9_]+/', '', $part);
if (strlen($partCleaned) !== strlen($part)) {
throw new EscapingException(
'Can not escape identifier. Passed value contains invalid characters. ' .
'Valid characters: A-Z, a-z, 0-9, Underscore'
);
}
$partsCleaned[] = '`' . $partCleaned . '`';
}
return implode('.', $partsCleaned);
}
/**
* @return void
*/
public function __clone()
{
$this->connection = null;
$this->transactionActive = false;
$this->config = clone $this->config;
}
/**
* @return void
*/
public function __destruct()
{
$this->disconnect();
}
/**
* @return void
*/
public function __wakeup()
{
}
/**
* @return array
*/
public function __sleep()
{
return [];
}
/**
* @param string $statement
* @param array $values
*
* @return mysqli_stmt
*/
private function getMysqliStatement($statement, array $values = [])
{
list($statement, $values) = $this->replaceArrayValues($statement, $values);
list($rebuildStatement, $bindValues, $parameterNames) = $this->replaceNamedParameters($statement, $values);
$query = $this->connection->prepare($rebuildStatement);
if ($query === false && $this->connection->errno === 2006) {
// Code 2006 = MySQL server has gone away
// Falls Verbindung in einen Timeout gelaufen ist
// => Verbindung wiederherstellen und Prepare erneut probieren
$this->disconnect();
$this->connect();
$query = $this->connection->prepare($rebuildStatement);
}
if ($query === false || !is_object($query)) {
throw new QueryFailureException(
sprintf(
'Database prepare failed. Error code #%s. Error message: %s',
$this->connection->errno,
$this->connection->error
),
(int)$this->connection->errno
);
}
$this->bindParametersToMysqliStatement($query, $bindValues, $parameterNames);
$query->execute();
if ($query->errno > 0) {
throw new QueryFailureException(
sprintf('Database query failed. Error code #%s. Error message: %s', $query->errno, $query->error),
(int)$query->errno
);
}
return $query;
}
/**
* @param string $statement
* @param array $values
*
* @return array
*/
private function replaceArrayValues($statement, array $values = [])
{
$parser = new MysqliArrayValueParser();
$result = $parser->rebuild($statement, $values);
return [$result['statement'], $result['values']];
}
/**
* @param string $statement
* @param array $values
*
* @return array
*/
private function replaceNamedParameters($statement, array $values = [])
{
$parser = new MysqliNamedParameterParser();
$result = $parser->rebuild($statement, $values);
return [$result['statement'], $result['values'], $result['params']];
}
/**
* @param mysqli_stmt $statement
* @param array $bindValues Values for binding
* @param array $parameterNames Original parameter names (for debugging only)
*
* @return void
*/
private function bindParametersToMysqliStatement($statement, $bindValues, $parameterNames)
{
if (empty($bindValues)) {
return;
}
$bindTypes = '';
foreach ($bindValues as $index => &$bindValue) {
if (is_bool($bindValue)) {
$bindValue = (int)$bindValue;
$bindTypes .= 'i'; // integer
continue;
}
if (is_float($bindValue)) {
$bindTypes .= 'd'; // double
continue;
}
if (is_array($bindValue)) {
throw new BindParameterException(sprintf(
'Can not bind parameter of type "array" to placeholder "%s".',
$parameterNames[$index]
));
}
if (is_object($bindValue)) {
throw new BindParameterException(sprintf(
'Can not bind parameter of type "object" to placeholder "%s".',
$parameterNames[$index]
));
}
$bindTypes .= 's'; // string
}
unset($bindValue);
$statement->bind_param($bindTypes, ...$bindValues);
}
/**
* @param $statement
* @param array $values
*
* @return mysqli_result
*/
private function getMysqliResult($statement, array $values = [])
{
$query = $this->getMysqliStatement($statement, $values);
$result = $query->get_result();
$query->close();
if ($result === false) {
throw new QueryFailureException(
sprintf('Database query failed. Error code #%s. Error message: %s', $query->errno, $query->error),
(int)$query->errno
);
}
return $result;
}
/**
* @param string $methodName
*
* @return void
*/
private function startProfiler($methodName)
{
if ($this->profiler === null) {
return;
}
$this->profiler->start(__CLASS__, $methodName);
}
/**
* @param string|null $statement
* @param array $values
*
* @return void
*/
private function finishProfiler($statement = null, array $values = [])
{
if ($this->profiler === null) {
return;
}
$this->profiler->finish($statement, $values);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace Xentral\Components\Database;
use Xentral\Components\Database\Adapter\MysqliAdapter;
use Xentral\Components\Database\Exception\ConfigException;
use Xentral\Components\Database\Profiler\Profiler;
use Xentral\Components\Database\SqlQuery\QueryFactory;
use Xentral\Components\Logger\Context\ContextHelper;
use Xentral\Components\Logger\MemoryLogger;
use Xentral\Core\DependencyInjection\ContainerInterface;
use Xentral\Core\DependencyInjection\ServiceContainer;
use Xentral\Core\LegacyConfig\ConfigLoader;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices()
{
return [
'Database' => 'onInitDatabase',
'DatabaseProfiler' => 'onGetDatabaseProfiler',
'MysqliAdapter' => 'onInitMysqliAdapter',
'QueryFactory' => 'onInitQueryFactory',
];
}
/**
* @param ServiceContainer $container
*
* @return Database
*/
public static function onInitDatabase(ServiceContainer $container)
{
return new Database($container->get('MysqliAdapter'), $container->get('QueryFactory'));
}
/**
* @return QueryFactory
*/
public static function onInitQueryFactory()
{
return new QueryFactory('mysql');
}
/**
* @param ServiceContainer $container
*
* @return Profiler
*/
public static function onGetDatabaseProfiler(ServiceContainer $container)
{
$request = $container->get('Request');
return new Profiler(new MemoryLogger(new ContextHelper($request)));
}
/**
* @param ContainerInterface $container
*
* @return MysqliAdapter
*/
public static function onInitMysqliAdapter(ContainerInterface $container)
{
$conf = ConfigLoader::load();
$dbHost = property_exists($conf, 'WFdbhost') ? $conf->WFdbhost : 'localhost';
$dbPort = property_exists($conf, 'WFdbport') ? $conf->WFdbport : 3306;
$dbName = property_exists($conf, 'WFdbname') ? $conf->WFdbname : null;
$dbUser = property_exists($conf, 'WFdbuser') ? $conf->WFdbuser : null;
$dbPass = property_exists($conf, 'WFdbpass') ? $conf->WFdbpass : null;
if (empty($dbName)) {
throw new ConfigException('Could not connect to database. Database name is missing or empty.');
}
if (empty($dbUser)) {
throw new ConfigException('Could not connect to database. Database user is missing or empty.');
}
if (empty($dbPass)) {
throw new ConfigException('Could not connect to database. Database password is missing or empty.');
}
$startupQueries = [
"SET NAMES 'utf8', " .
"CHARACTER SET 'utf8', " .
"lc_time_names = 'de_DE', " .
"SESSION sql_mode = '', " .
"SESSION sql_big_selects = 1;",
];
$config = new DatabaseConfig($dbHost, $dbUser, $dbPass, $dbName, 'utf8', $dbPort, $startupQueries);
// Profiler aktivieren
// Kann mit $container->get('DatabaseProfiler')->getContexts() abgefragt werden
$profiler = $container->get('DatabaseProfiler');
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE === true) {
$profiler->setActive(true);
}
return new MysqliAdapter($config, $profiler);
}
}
+356
View File
@@ -0,0 +1,356 @@
<?php /** @noinspection PhpInconsistentReturnPointsInspection */
namespace Xentral\Components\Database;
use Generator;
use Xentral\Components\Database\Adapter\AdapterInterface;
use Xentral\Components\Database\Exception\EscapingException;
use Xentral\Components\Database\Exception\TransactionException;
use Xentral\Components\Database\SqlQuery\DeleteQuery;
use Xentral\Components\Database\SqlQuery\InsertQuery;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Components\Database\SqlQuery\QueryFactory;
use Xentral\Components\Database\SqlQuery\UpdateQuery;
final class Database
{
/** @var AdapterInterface $adapter */
private $adapter;
/** @var QueryFactory $queryFactory */
private $queryFactory;
/**
* @param AdapterInterface $adapter
* @param QueryFactory $queryFactory
*/
public function __construct(AdapterInterface $adapter, QueryFactory $queryFactory)
{
$this->adapter = $adapter;
$this->queryFactory = $queryFactory;
}
/**
* Aura.SqlQuery
*/
/**
* @return SelectQuery
*/
public function select()
{
return $this->queryFactory->newSelect();
}
/**
* @return InsertQuery
*/
public function insert()
{
return $this->queryFactory->newInsert();
}
/**
* @return UpdateQuery
*/
public function update()
{
return $this->queryFactory->newUpdate();
}
/**
* @return DeleteQuery
*/
public function delete()
{
return $this->queryFactory->newDelete();
}
/**
* ENDE: Aura.SqlQuery
*/
/**
* Close database connection
*
* @return void
*/
public function close()
{
$this->adapter->disconnect();
}
/**
* Executes simple queries without named parameters
*
* Use self::perform() for queries with named parameters.
*
* @param string $query
*
* @return void
*/
public function exec($query)
{
$this->adapter->perform($query, []);
}
/**
* @return int
*/
public function lastInsertId()
{
return (int)$this->adapter->lastInsertId();
}
/**
* @throws TransactionException If transaction is already started
*
* @return void
*/
public function beginTransaction()
{
$this->adapter->beginTransaction();
}
/**
* @return void
*/
public function commit()
{
$this->adapter->commit();
}
/**
* @return void
*/
public function rollBack()
{
$this->adapter->rollBack();
}
/**
* @return bool
*/
public function inTransaction()
{
return $this->adapter->inTransaction();
}
/**
* @param string $query
* @param array $values
*
* @return array Empty array on empty result
*/
public function fetchAll($query, array $values = [])
{
return $this->adapter->fetchAll($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return array Empty array on empty result
*/
public function fetchAssoc($query, array $values = [])
{
return $this->adapter->fetchAssoc($query, $values);
}
/**
* @param string $query
* @param array $values
* @param bool $includeGroupColumn
*
* @return array Empty array on empty result
*/
public function fetchGroup($query, array $values = [], $includeGroupColumn = false)
{
return $this->adapter->fetchGroup($query, $values, (bool)$includeGroupColumn);
}
/**
* @param string $query
* @param array $values
*
* @return array Empty array on empty result
*/
public function fetchRow($query, array $values = [])
{
return $this->adapter->fetchRow($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return array Empty array on empty result
*/
public function fetchPairs($query, array $values = [])
{
return $this->adapter->fetchPairs($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return int|float|string|false false on empty result
*/
public function fetchValue($query, array $values = [])
{
return $this->adapter->fetchValue($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return int
*/
public function fetchAffected($query, array $values = [])
{
return $this->adapter->fetchAffected($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return array Empty array on empty result
*/
public function fetchCol($query, array $values = [])
{
return $this->adapter->fetchCol($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return Generator
*/
public function yieldAll($query, array $values = [])
{
return $this->adapter->yieldAll($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return Generator
*/
public function yieldAssoc($query, array $values = [])
{
return $this->adapter->yieldAssoc($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return Generator
*/
public function yieldPairs($query, array $values = [])
{
return $this->adapter->yieldPairs($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return Generator
*/
public function yieldCol($query, array $values = [])
{
return $this->adapter->yieldCol($query, $values);
}
/**
* @param string $query
* @param array $values
*
* @return void
*/
public function perform($query, array $values = [])
{
$this->adapter->perform($query, $values);
}
/**
* Escapes values for "BOOLEAN" and (TINY)INT columns
*
* @param bool|null $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeBool($value, $isNullable = false)
{
return $this->adapter->escapeBool($value, $isNullable);
}
/**
* Escapes values for INT columns
*
* @param mixed $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeInt($value, $isNullable = false)
{
return $this->adapter->escapeInt($value, $isNullable);
}
/**
* Escapes values for FLOAT, DOUBLE and REAL columns
*
* @param mixed $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeDecimal($value, $isNullable = false)
{
return $this->adapter->escapeDecimal($value, $isNullable);
}
/**
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
*
* @param mixed $value
* @param bool $isNullable
*
* @throws EscapingException
*
* @return string
*/
public function escapeString($value, $isNullable = false)
{
return $this->adapter->escapeString($value, $isNullable);
}
/**
* Escapes and quotes an identifier (column or table name)
*
* @param string $value
*
* @throws EscapingException
*
* @return string
*/
public function escapeIdentifier($value)
{
return $this->adapter->escapeIdentifier($value);
}
}
@@ -0,0 +1,146 @@
<?php
namespace Xentral\Components\Database;
final class DatabaseConfig
{
/** @var string $hostname */
private $hostname;
/** @var string $username */
private $username;
/** @var string $password */
private $password;
/** @var string $database */
private $database;
/** @var string $charset */
private $charset;
/** @var int $port */
private $port;
/** @var array $queries */
private $queries;
/**
* @param string $hostname
* @param string $username
* @param string $password
* @param string $database
* @param string|null $charset
* @param int|null $port
* @param array $queries
*/
public function __construct(
$hostname,
$username,
$password,
$database,
$charset = null,
$port = null,
array $queries = []
) {
$this->hostname = (string)$hostname;
$this->username = (string)$username;
$this->password = (string)$password;
$this->database = (string)$database;
$this->charset = $charset !== null ? (string)$charset : 'utf8';
$this->port = $port !== null ? (int)$port : 3306;
$this->queries = $queries;
}
/**
* @param array $config
*
* @return DatabaseConfig
*/
public static function fromArray(array $config)
{
return new DatabaseConfig(
$config['hostname'],
$config['username'],
$config['password'],
$config['database'],
isset($config['charset']) ? $config['charset'] : null,
isset($config['port']) ? $config['port'] : null,
isset($config['queries']) ? $config['queries'] : []
);
}
/**
* @return string
*/
public function getHostname()
{
return $this->hostname;
}
/**
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* @return string
*/
public function getDatabase()
{
return $this->database;
}
/**
* @return string
*/
public function getCharset()
{
return $this->charset;
}
/**
* @return int
*/
public function getPort()
{
return $this->port;
}
/**
* @return array
*/
public function getQueries()
{
return $this->queries;
}
/**
* @return array
*/
public function __debugInfo()
{
return [
'args' => [
'hostname' => $this->hostname,
'username' => '****',
'password' => '****',
'database' => $this->database,
'charset' => $this->charset,
'port' => $this->port,
'queries' => $this->queries,
],
];
}
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class BindParameterException extends \RuntimeException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class ConfigException extends \InvalidArgumentException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class ConnectionException extends \RuntimeException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,12 @@
<?php
namespace Xentral\Components\Database\Exception;
use RuntimeException;
/**
* @deprecated Will be removed in 19.4
*/
class DatabaseException extends RuntimeException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Database\Exception;
use Xentral\Core\Exception\ComponentExceptionInterface;
interface DatabaseExceptionInterface extends ComponentExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class EscapingException extends \RuntimeException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class MissingParameterException extends \RuntimeException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class NotConnectedException extends \LogicException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class QueryFailureException extends \RuntimeException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Components\Database\Exception;
class TransactionException extends \RuntimeException implements DatabaseExceptionInterface
{
}
@@ -0,0 +1,66 @@
<?php
namespace Xentral\Components\Database\Parser;
/**
* @example
* self::rebuild('SELECT * FROM foo WHERE id IN (:ids)', ['ids' => [1, 2, 3]])
* Erzeugt:
* [
* 'statement' => 'SELECT * FROM foo WHERE id IN (:ids_expl_0_, :ids_expl_1_, :ids_expl_2_)',
* 'values' => [
* '_ids_expl_0_' => 1,
* '_ids_expl_1_' => 2,
* '_ids_expl_2_' => 3,
* ]
* ]
*/
final class MysqliArrayValueParser implements ParserInterface
{
/**
* @param string $statement
* @param array $values
*
* @return array
* - Array key 'statement' contains the rebuild statement
* - Array key 'values' contains the rebuild bind parameters
*/
public function rebuild($statement, array $values = [])
{
return $this->replaceArrayValues($statement, $values);
}
/**
* @param string $statement
* @param array $values
*
* @return array
*/
private function replaceArrayValues($statement, array $values = [])
{
foreach ($values as $paramName => $paramValue) {
if (is_array($paramValue)) {
$counter = 0;
$additionalParams = [];
foreach ($paramValue as $arrayValue) {
$additionalParamName = '_' . $paramName . '_expl_' . $counter . '_';
$additionalParams[] = ':' . $additionalParamName;
$values[$additionalParamName] = $arrayValue;
$counter++;
}
// Replace original named parameter by exploded parameters in statement
$replaceString = implode(', ', $additionalParams);
$statement = str_replace(':' . $paramName, $replaceString, $statement);
// Remove original parameter value
unset($values[$paramName]);
}
}
return [
'statement' => $statement,
'values' => $values,
];
}
}
@@ -0,0 +1,79 @@
<?php
namespace Xentral\Components\Database\Parser;
use Xentral\Components\Database\Exception\MissingParameterException;
/**
* Responsibility of this class is to make sql statement and bind values compatible with mysqli
*
* (Mysqli does not support named parameters)
*
* It does this by:
* - Replacing named parameters (:param) by ?-Placeholder (in statement)
* - Rearranging bind values in order of appearance of named parameters
*/
final class MysqliNamedParameterParser implements ParserInterface
{
/**
* @param string $statement
* @param array $values
*
* @return array
*/
public function rebuild($statement, array $values = [])
{
return $this->replaceNamedParameters($statement, $values);
}
/**
* @param string $statement
* @param array $values
*
* @throws MissingParameterException
*
* @return array
*/
private function replaceNamedParameters($statement, array $values = [])
{
$result = [
'statement' => $statement,
'values' => [],
'params' => [],
];
if (empty($values)) {
return $result;
}
// Split statement on named parameters
$parts = preg_split('/(:[a-zA-Z0-9_]+)/um', $statement, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
foreach ($parts as &$part) {
if (strpos($part, ':') !== 0) {
continue; // SQL part does not contain named parameter
}
$parameterName = substr_replace($part, '', 0, 1);
if (!array_key_exists($parameterName, $values)) {
throw new MissingParameterException(sprintf(
'Parameter "%s" is missing from the bound values',
$parameterName
));
}
// Push values in same order of parameters for binding
$result['values'][] = $values[$parameterName];
$result['params'][] = $parameterName; // For debugging only
// Replace named parameter by ?-Placeholder
$part = '?';
}
unset($part);
// Rebuild statement from (changed) parts
$result['statement'] = implode('', $parts);
return $result;
}
}
@@ -0,0 +1,16 @@
<?php
namespace Xentral\Components\Database\Parser;
interface ParserInterface
{
/**
* @param string $statement
* @param array $values
*
* @return array
* - Array key 'statement' contains the corrected statement
* - Array key 'values' contains the corrected bind values
*/
public function rebuild($statement, array $values = []);
}
@@ -0,0 +1,135 @@
<?php
namespace Xentral\Components\Database\Profiler;
use Exception;
use Xentral\Components\Logger\LoggerInterface;
final class Profiler implements ProfilerInterface
{
/** @var LoggerInterface $logger */
private $logger;
/** @var bool $active */
private $active = false;
/** @var string $logLevel */
private $logLevel = 'debug';
/** @var string $logFormat */
private $logFormat = "{method} ({duration}): {statement} \n{backtrace}";
/** @var array $context */
private $context = [];
/** @var array $contexts */
private $contexts = [];
/**
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @param string $className
* @param string $methodName
*
* @return void
*/
public function start($className, $methodName)
{
if (!$this->active) {
return;
}
$this->context = [
'class' => $className,
'method' => $methodName,
'start' => microtime(true),
];
}
/**
* @param string|null $statement
* @param array $values
*
* @return void
*/
public function finish($statement = null, array $values = [])
{
if (!$this->active) {
return;
}
$finish = microtime(true);
$exception = new Exception();
$this->context['finish'] = $finish;
$this->context['duration_real'] = $finish - $this->context['start'];
$this->context['duration'] = sprintf('%.6f', $this->context['duration_real']) . ' seconds';
$this->context['statement'] = $statement;
$this->context['bindings'] = $values;
$this->context['backtrace'] = $exception->getTraceAsString();
$this->logger->log($this->logLevel, $this->logFormat, $this->context);
$this->contexts[] = $this->context;
$this->context = [];
}
/**
* @return bool
*/
public function isActive()
{
return $this->active;
}
/**
* @param bool $active
*
* @return void
*/
public function setActive($active)
{
$this->active = (bool)$active;
}
/**
* /**
* @return string
*/
public function getLogLevel()
{
return $this->logLevel;
}
/**
* @param string $logLevel
*
* @return void
*/
public function setLogLevel($logLevel)
{
$this->logLevel = (string)$logLevel;
}
/**
* @return LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* @return array
*/
public function getContexts()
{
return $this->contexts;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Xentral\Components\Database\Profiler;
use Xentral\Components\Logger\LoggerInterface;
interface ProfilerInterface
{
/**
* @param string $className
* @param string $methodName
*
* @return void
*/
public function start($className, $methodName);
/**
* @param string|null $statement
* @param array $values
*
* @return void
*/
public function finish($statement = null, array $values = []);
/**
* @return bool
*/
public function isActive();
/**
* @param bool $active
*
* @return void
*/
public function setActive($active);
/**
/**
* @return string
*/
public function getLogLevel();
/**
* @param string $logLevel
*
* @return void
*/
public function setLogLevel($logLevel);
/**
* @return LoggerInterface
*/
public function getLogger();
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Database\SqlQuery;
use Aura\SqlQuery\Mysql\Delete;
final class DeleteQuery extends Delete
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Database\SqlQuery;
use Aura\SqlQuery\Mysql\Insert;
final class InsertQuery extends Insert
{
}
@@ -0,0 +1,59 @@
<?php
namespace Xentral\Components\Database\SqlQuery;
use Aura\SqlQuery\AbstractQuery;
use Aura\SqlQuery\QueryFactory as AuraQueryFactory;
final class QueryFactory extends AuraQueryFactory
{
/**
* @return SelectQuery
*/
public function newSelect()
{
return $this->newInstance('Select');
}
/**
* @return InsertQuery
*/
public function newInsert()
{
$insert = $this->newInstance('Insert');
$insert->setLastInsertIdNames($this->last_insert_id_names);
return $insert;
}
/**
* @return UpdateQuery
*/
public function newUpdate()
{
return $this->newInstance('Update');
}
/**
* @return DeleteQuery
*/
public function newDelete()
{
return $this->newInstance('Delete');
}
/**
* @param string $query The query object type.
*
* @return AbstractQuery
*/
protected function newInstance($query)
{
$class = "Xentral\\Components\\Database\\SqlQuery\\{$query}Query";
return new $class(
$this->getQuoter(),
$this->newSeqBindPrefix()
);
}
}
@@ -0,0 +1,85 @@
<?php
namespace Xentral\Components\Database\SqlQuery;
use Aura\SqlQuery\Mysql\Select;
use Closure;
final class SelectQuery extends Select
{
/**
* @return bool
*/
public function hasOrderBy()
{
return !empty($this->order_by);
}
/**
* @param string $andor
* @param array|callable $args
*
* @return Select
*/
protected function addWhere($andor, $args)
{
if ($args[0] instanceof Closure) {
$this->addClauseCondClosure('where', $andor, $args[0]);
return $this;
}
return parent::addWhere($andor, $args);
}
/**
* Aura.SqlQuery 2.x unterstützt keine Klammersetzung in WHERE-Bedingungen
*
* @see https://github.com/auraphp/Aura.SqlQuery/issues/97
*
* Feature aus Version 3 importiert: https://github.com/auraphp/Aura.SqlQuery/pull/136/files
*
* @copyright Paul M. Jones
* @license MIT
*
* @param string $clause
* @param string $andor
* @param callable $closure
*/
protected function addClauseCondClosure($clause, $andor, $closure)
{
// retain the prior set of conditions, and temporarily reset the clause
// for the closure to work with (otherwise there will be an extraneous
// opening AND/OR keyword)
$set = $this->$clause;
$this->$clause = [];
// invoke the closure, which will re-populate the $this->$clause
$closure($this);
// are there new clause elements?
if (!$this->$clause) {
// no: restore the old ones, and done
$this->$clause = $set;
return;
}
// append an opening parenthesis to the prior set of conditions,
// with AND/OR as needed ...
if ($set) {
$set[] = "{$andor} (";
} else {
$set[] = "(";
}
// append the new conditions to the set, with indenting
foreach ($this->$clause as $cond) {
$set[] = " {$cond}";
}
$set[] = ")";
// ... then put the full set of conditions back into $this->$clause
$this->$clause = $set;
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Database\SqlQuery;
use Aura\SqlQuery\Mysql\Update;
final class UpdateQuery extends Update
{
}
@@ -0,0 +1,77 @@
# Data Manipulation
## `perform()`
Für alle SQL Anweisung außer `SELECT`. Die Methode gibt nichts zurück. Sollte die Ausführung
fehlschlagen, so wird eine Exception geworfen.
```php
$sql = 'UPDATE foo SET bar = :bar WHERE id = :id';
$values = [
'bar' => 'baz',
'id' => 123,
];
$db->perform($sql, $values);
```
## `fetchAffected()`
Für `INSERT`, `REPLACE`, `UPDATE` und `DELETE` Anweisungen. Gibt die Anzahl der betroffenen Datensätze zurück.
```php
$sql = 'UPDATE foo SET bar = :bar WHERE 1';
$values = [
'bar' => 'baz',
];
echo $db->fetchAffected($sql, $values);
```
###### Ausgabe
```
42
```
## `lastInsertId()`
Gibt den zuletzt erzeugten Auto-Increment-Wert zurück.
```php
$sql = 'INSERT INTO foo (id, bar) VALUES (NULL, :bar)';
$values = [
'bar' => 'baz',
];
$db->perform($sql, $values);
// $db->fetchAffected($sql, $values); // Alternative
echo $db->lastInsertId();
```
###### Ausgabe
```
123
```
## Multiple-Row-Insert
```php
$sql = 'INSERT INTO foo (id, bar) VALUES (NULL, :bar1, :baz1), (NULL, :bar2, :baz2), (NULL, :bar3, :baz3)';
$values = [
'bar1' => 'bar',
'baz1' => 'baz',
'bar2' => 'barbar',
'baz2' => 'bazbaz',
'bar3' => 'barbarbar',
'baz3' => 'bazbazbaz',
];
$db->perform($sql, $values);
// $db->fetchAffected($sql, $values); // Alternative; Rückgabe wäre `3`
```
@@ -0,0 +1,158 @@
# Datensätze abrufen
## `fetchAll()`
```php
$data = $db->fetchAll('SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.id > 5');
```
```
array (
0 =>
array (
'id' => 6,
'typ' => 'produkt',
'name_de' => 'LED Anzeige RLED 24-8',
),
1 =>
array (
'id' => 7,
'typ' => 'produkt',
'name_de' => 'Schalter S3 24V 5A',
),
...
)
```
## `fetchAssoc()`
Wie `fetchAll()` allerdings wird das Ergebnis der ersten Spalte als Index verwendet.
```php
$data = $db->fetchAssoc('SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.id > 5');
```
```
array (
6 =>
array (
'id' => 6,
'typ' => 'produkt',
'name_de' => 'LED Anzeige RLED 24-8',
),
7 =>
array (
'id' => 7,
'typ' => 'produkt',
'name_de' => 'Schalter S3 24V 5A',
),
...
)
```
## `fetchRow()`
Liefert die Ergebnisse der ersten Zeile als assoziatives Array. Die Spaltennamen werden als Index verwendet.
```php
$data = $db->fetchRow('SELECT a.id, a.name_de, a.name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
```
```
array (
'id' => 6,
'name_de' => 'LED Anzeige RLED 24-8',
'name_en' => '',
'logdatei' => '2015-10-26 17:26:27',
)
```
## `fetchValue()`
Liefert nur das Ergebnis der ersten Zeile und Spalte zurück.
```php
$data = $db->fetchValue('SELECT a.name_de, name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
```
```
'LED Anzeige RLED 24-8'
```
## `fetchCol()`
Liefert nur die Ergebnisse der ersten Spalte zurück.
```php
$data = $db->fetchCol('SELECT a.name_de, a.name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
```
```
array (
0 => 'LED Anzeige RLED 24-8',
1 => 'Schalter S3 24V 5A',
2 => 'Gehäuse GHK5 20x30x10',
...
)
```
## `fetchPairs()`
Gibt ein (eindimentionales) assoziatives Array zurück. Der Wert der ersten Spalte wird als Index verwendet und die zweite Spalte als Wert.
**Erwartet werden genau zwei Spalten, andernfalls wird eine Exception geworfen.**
```php
$data = $db->fetchPairs('SELECT a.nummer, a.name_de FROM artikel AS a WHERE a.id > 5');
```
```
array (
700006 => 'LED Anzeige RLED 24-8',
700007 => 'Schalter S3 24V 5A',
700008 => 'Gehäuse GHK5 20x30x10',
...
)
```
## `fetchGroup()`
Verhält sich wie `fetchAssoc()`; mit der Ausnahme dass die Werte der ersten Spalte gruppiert werden.
```php
$data = $db->fetchGroup(
'SELECT a.typ, a.nummer, a.name_de, a.logdatei FROM artikel AS a WHERE a.id > 5'
);
```
```
array (
'produkt' =>
array (
0 =>
array (
'typ' => 'produkt',
'nummer' => '700006',
'name_de' => 'LED Anzeige RLED 24-8',
'logdatei' => '2015-10-26 17:26:27',
),
1 =>
array (
'typ' => 'produkt',
'nummer' => '700007',
'name_de' => 'Schalter S3 24V 5A',
'logdatei' => '2015-10-26 17:26:59',
),
2 =>
array (
'typ' => 'produkt',
'nummer' => '700008',
'name_de' => 'Gehäuse GHK5 20x30x10',
'logdatei' => '2018-05-23 05:18:52',
),
),
'gebuehr' =>
array (
0 =>
array (
'typ' => 'gebuehr',
'nummer' => '100001',
'name_de' => 'Versandkosten',
'logdatei' => '2018-06-17 10:09:36',
),
),
)
```
+51
View File
@@ -0,0 +1,51 @@
# Database-Komponente
###### Verwendete Libraries
* **Aura.SqlQuery**:
* Composer: `aura/sqlquery:2.7.*`
* Packagist: https://packagist.org/packages/aura/sqlquery
* GitHub: https://github.com/auraphp/Aura.SqlQuery
* Docs: https://github.com/auraphp/Aura.SqlQuery/tree/2.x
* **~~Aura.Sql~~** Wird in 19.4 entfernt
* Composer: `aura/sql:3.*`
* Packagist: https://packagist.org/packages/aura/sql
* GitHub: https://github.com/auraphp/Aura.Sql
* Docs: https://github.com/auraphp/Aura.Sql/blob/3.x/docs/index.md
##### Database-Komponente aus Container holen
```php
$db = $container->get('Database');
```
Im alten Bereich:
```php
$db = $this->app->Container->get('Database');
```
## Themen
* [Datensätze abrufen (fetch)](fetch_results.md)
* [Datensätze abrufen mit Generatoren (yield)](yield_results.md)
* [Datensätze ändern](data_manipulation.md)
* [Transaktionen](transactions.md)
* [Named Parameter / Prepared Statements](named_parameter.md)
* [SQL Query Builder](query_builder.md)
## Exceptions
Die Database-Komponente verwendet intern `mysqli`. Im Unterschied zu `mysqli` werden in Fehlerfällen aber
Exceptions geworfen; z.B.:
* Wenn die Verbindung zur Datenbank fehlschlägt => `ConnectionException`
* Wenn ein SQL-Statement fehlerhaft ist oder aus anderen Gründen nicht erfolgreich ausgeführt
werden kann => `QueryFailureException`
* Wenn `Named Parameter` fehlen => `MissingParameterException`
##### ExceptionInterface
Alle Exceptions die von der Database-Komponente geworfen werden implementieren das
`\Xentral\Components\Database\Exception\DatabaseExceptionInterface` Interface.
@@ -0,0 +1,44 @@
# Named Parameter
`perform()`, `fetch*()` und `yield*()`-Methoden nehmen als zweiten Parameter ein assoziatives Array entgegen.
Mit diesem Array werden lassen sich Werte als *Named Parameter* 'binden'.
```php
$sql =
'SELECT a.id, a.nummer, a.name_de
FROM artikel AS a
WHERE a.typ IN (:types)
AND a.nummer LIKE :nummers
LIMIT :start, :length';
$values = [
'types' => ['produkt', 'gebuehr'],
'nummers' => '7000%',
'start' => 0,
'length' => 3,
];
$data = $db->fetchAll($sql, $values);
```
```
array (
0 =>
array (
'id' => 1,
'nummer' => '700001',
'name_de' => 'Schraube M10x20',
),
1 =>
array (
'id' => 2,
'nummer' => '700002',
'name_de' => 'Sechskant-Mutter M10',
),
2 =>
array (
'id' => 3,
'nummer' => '700003',
'name_de' => 'Schalthebel 20x10',
),
)
```
@@ -0,0 +1,13 @@
# Profiler
```php
<?php
/** @var \Xentral\Components\Database\Profiler\Profiler $profiler */
$profiler = $container->get('DatabaseProfiler');
$profiler->setActive(true);
$database->fetchAll($sql);
var_dump($profiler->getContexts());
```
@@ -0,0 +1,97 @@
# SQL Query Builder
###### Verwendete Libraries
* **Aura.SqlQuery**:
* Composer: `aura/sqlquery:2.7.*`
* Packagist: https://packagist.org/packages/aura/sqlquery
* GitHub: https://github.com/auraphp/Aura.SqlQuery
* Docs: https://github.com/auraphp/Aura.SqlQuery/tree/2.x
## Query Builder erzeugen
```php
$select = $db->select();
$update = $db->update();
$insert = $db->insert();
$delete = $db->delete();
```
## SELECT-Query
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#select
###### Beispiel mit Named-Placeholder
```php
$select = $db->select();
$select
->cols(['u.id', 'u.description'])
->from('user AS u')
->where('u.id = :user_id')
->bindValue('user_id', 1);
$result = $db->fetchAll(
$select->getStatement(),
$select->getBindValues()
);
var_export($result);
// array(
// 0 => array(
// 'id' => 1,
// 'description' => 'Administrator',
// ),
// )
```
###### Alternative mit ?-Placeholder
```php
$select = $db->select();
$select
->cols(['u.id', 'u.description'])
->from('user AS u')
->where('u.id = ?', 1);
```
###### Beispiel mit Verschachtelung im WHERE
```php
$select = $db->select();
$select
->cols(['u.id', 'u.description'])
->from('user AS u')
->where('u.activ = ?', 1)
->where(function (SelectQuery $query) {
$query->where('u.type = ?', 'admin')
->orWhere('u.type = ?', 'standard');
});
echo $select->getStatement();
```
```sql
SELECT
`u`.`id`,
`u`.`description`
FROM
`user` AS `u`
WHERE
`u`.`activ` = :_1_1_
AND (
`u`.`type` = :_1_2_
OR `u`.`type` = :_1_3_
)
```
## INSERT-Query
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#insert
## UPDATE-Query
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#update
## DELETE-Query
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#delete
@@ -0,0 +1,25 @@
# Transaktionnen
## Transaktion starten
```php
$db->beginTransaction();
```
## Transaktion übernehmen / Commit
```php
$db->commit();
```
## Transaktion zurücknehmen / Rollback
```php
$db->rollback();
```
## Prüfen ob Transaktion gestartet ist
```php
$db->inTransaction();
```
@@ -0,0 +1,107 @@
# Datensätze abrufen mit Generatoren
Um den Arbeitsspeicherverbrauch gering zu halten bieten sich zum Iterieren von großen Datenmengen
Generatoren an: https://www.php.net/manual/de/language.generators.overview.php
Die Database-Komponente stellt für diesen Zweck `yield`-Methoden zur Verfügung.
## `yieldAll()`
Wie `fetchAll()`; jede Zeile ist ein assoziatives Array.
```php
$statement = 'SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
$bindValues = ['typ' => 'produkt'];
foreach ($db->yieldAll($statement, $bindValues) as $row) {
var_dump($row);
}
```
```
array (size=3)
'id' => int 2
'typ' => string 'produkt' (length=7)
'name_de' => string 'Sechskant-Mutter M10' (length=20)
array (size=3)
'id' => int 3
'typ' => string 'produkt' (length=7)
'name_de' => string 'Schalthebel 20x10' (length=17)
```
## `yieldAssoc()`
Wie `fetchAssoc()`; jede Zeile ist ein assoziatives Array; der Key beinhaltet den Wert der ersten Spalte
```php
$statement = 'SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
$bindValues = ['typ' => 'produkt'];
foreach ($db->yieldAssoc($statement, $bindValues) as $key => $row) {
var_dump($key);
var_dump($row);
}
```
```
int 2
array (size=3)
'id' => int 2
'typ' => string 'produkt' (length=7)
'name_de' => string 'Sechskant-Mutter M10' (length=20)
int 3
array (size=3)
'id' => int 3
'typ' => string 'produkt' (length=7)
'name_de' => string 'Schalthebel 20x10' (length=17)
```
## `yieldPairs()`
Wie `fetchPairs()`; jede Zeile besteht aus Key-Value-Paaren; der Key beinhaltet den Inhalt der ersten Spalte;
der Wert den Inhalt der zweiten Spalte.
**Erwartet werden genau zwei Spalten, andernfalls wird eine Exception geworfen.**
```php
$statement = 'SELECT a.id, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
$bindValues = ['typ' => 'produkt'];
foreach ($db->yieldPairs($statement, $bindValues) as $key => $value) {
var_dump($key);
var_dump($value);
}
```
```
int 2
string 'Sechskant-Mutter M10' (length=20)
int 3
string 'Schalthebel 20x10' (length=17)
```
## `yieldCol()`
Wie `fetchCol()`; jede Zeile beinhaltet nur den Wert der ersten Spalte.
```php
$statement = 'SELECT a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
$bindValues = ['typ' => 'produkt'];
foreach ($db->yieldCol($statement, $bindValues) as $key => $value) {
var_dump($key);
var_dump($value);
}
```
```
int 0
string 'Sechskant-Mutter M10' (length=20)
int 1
string 'Schalthebel 20x10' (length=17)
```
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Xentral\Components\EnvironmentConfig;
use SplFileInfo;
use Xentral\Core\DependencyInjection\ContainerInterface;
use Config;
use License;
use Xentral\Modules\Api\LegacyBridge\LegacyApplication;
class Bootstrap
{
/**
* @return array
*/
public static function registerServices(): array
{
return [
'EnvironmentConfig' => 'onInitEnvironmentConfig',
];
}
/**
* @param ContainerInterface $container
*
* @return EnvironmentConfig
*/
public static function onInitEnvironmentConfig(ContainerInterface $container): EnvironmentConfig
{
$provider = self::onInitEnvironmentConfigProvider($container);
return $provider->createEnvironmentConfig();
}
/**
* @param ContainerInterface $container
*
* @return EnvironmentConfigProvider
*/
private static function onInitEnvironmentConfigProvider(ContainerInterface $container): EnvironmentConfigProvider
{
/** @var LegacyApplication $app */
$app = $container->get('LegacyApplication');
if (!class_exists('License')) {
$test = dirname(__DIR__, 3) . '/phpwf/plugins/class.license.php';
$file = new SplFileInfo($test);
if ($file->isFile()) {
include $test;
}
}
return new EnvironmentConfigProvider(new License(), $app->Conf);
}
}
@@ -0,0 +1,233 @@
<?php
namespace Xentral\Components\EnvironmentConfig;
final class EnvironmentConfig
{
/**
* @var string $databaseHost
*/
private $databaseHost;
/**
* @var string $databaseName
*/
private $databaseName;
/**
* @var string $databaseUser
*/
private $databaseUser;
/**
* @var string $databasePassword
*/
private $databasePassword;
/**
* @var int $databasePort
*/
private $databasePort;
/**
* @var string $userdataDirectoryPath
*/
private $userdataDirectoryPath;
/**
* @var array|null $ioncubeSystemInformation
*/
private $ioncubeSystemInformation;
/**
* @param string $databaseHost
* @param string $databaseName
* @param string $databaseUser
* @param string $databasePassword
* @param int $databasePort
* @param string $userdataDirectoryPath
* @param array $ioncubeSystemInformation
*/
public function __construct(
string $databaseHost,
string $databaseName,
string $databaseUser,
string $databasePassword,
int $databasePort,
string $userdataDirectoryPath,
?array $ioncubeSystemInformation
) {
$this->databaseHost = $databaseHost;
$this->databaseName = $databaseName;
$this->databaseUser = $databaseUser;
$this->databasePassword = $databasePassword;
$this->databasePort = $databasePort;
$this->userdataDirectoryPath = $userdataDirectoryPath;
$this->ioncubeSystemInformation = $ioncubeSystemInformation;
}
/**
* @return string
*/
public function getDatabaseHost(): string
{
return $this->databaseHost;
}
/**
* @return string
*/
public function getDatabaseName(): string
{
return $this->databaseName;
}
/**
* @return string
*/
public function getDatabaseUser(): string
{
return $this->databaseUser;
}
/**
* @return string
*/
public function getDatabasePassword(): string
{
return $this->databasePassword;
}
/**
* @return int
*/
public function getDatabasePort(): int
{
return $this->databasePort;
}
/**
* @return string
*/
public function getUserdataDirectoryPath(): string
{
return $this->userdataDirectoryPath;
}
/**
* @return ?array
*/
public function getIoncubeSystemInformation(): ?array
{
return $this->ioncubeSystemInformation;
}
/**
* @return bool
*/
public function isSystemHostedOnCloud(): bool
{
return !empty($this->ioncubeSystemInformation['iscloud']['value']);
}
/**
* @return bool
*/
public function isSystemFlaggedAsDevelopmentVersion(): bool
{
return !empty($this->ioncubeSystemInformation['isdevelopmentversion']['value']);
}
/**
* @return bool
*/
public function isSystemFlaggedAsTestVersion(): bool
{
return !empty($this->ioncubeSystemInformation['testlizenz']['value']);
}
/**
* @return int
*/
public function getMaxUser(): int
{
if (!isset($this->ioncubeSystemInformation['maxuser']['value'])) {
return 0;
}
return (int)$this->ioncubeSystemInformation['maxuser']['value'];
}
/**
* @return int
*/
public function getMaxLightUser(): int
{
if (!isset($this->ioncubeSystemInformation['maxlightuser']['value'])) {
return 0;
}
return (int)$this->ioncubeSystemInformation['maxlightuser']['value'];
}
/**
* @return int|null
*/
public function getExpirationTimeStamp(): ?int
{
if (!isset($this->ioncubeSystemInformation['expdate']['value'])) {
return 0;
}
return (int)$this->ioncubeSystemInformation['expdate']['value'];
}
/**
* @param string $name
*
* @return string|null
*/
public function getValueOfSpecificIoncubeSystemInformation(string $name): ?string
{
if ($this->ioncubeSystemInformation === null) {
return null;
}
if (array_key_exists($name, $this->ioncubeSystemInformation)) {
return $this->ioncubeSystemInformation[$name]['value'];
}
return null;
}
/**
* @return array
*/
public function getSystemFallbackEmailAddresses(): array
{
$emailAddresses = [];
$mailAddressSelfBuyCustomer = (string)$this->getValueOfSpecificIoncubeSystemInformation('buyemail');
if ($mailAddressSelfBuyCustomer !== '') {
$emailAddresses[] = $mailAddressSelfBuyCustomer;
}
$mailAddressCustomerLicence = (string)$this->getValueOfSpecificIoncubeSystemInformation('emaillicence');
if ($mailAddressCustomerLicence !== ''
&& strpos($mailAddressCustomerLicence, '@') !== false
&& strpos($mailAddressCustomerLicence, '@xentral.com') === false
&& strpos($mailAddressCustomerLicence, '@xentral.biz') === false) {
$emailAddresses[] = $mailAddressCustomerLicence;
}
//in old licences email-address of customer can be insert in name instead email
$nameCustomerLicence = (string)$this->getValueOfSpecificIoncubeSystemInformation('namelicence');
if ($nameCustomerLicence !== ''
&& strpos($nameCustomerLicence, '@') !== false
&& strpos($nameCustomerLicence, '@xentral.com') === false
&& strpos($nameCustomerLicence, '@xentral.biz') === false) {
$emailAddresses[] = $nameCustomerLicence;
}
return $emailAddresses;
}
}
@@ -0,0 +1,39 @@
<?php
namespace Xentral\Components\EnvironmentConfig;
use Config;
use License;
final class EnvironmentConfigProvider
{
/** @var License $license */
private $license;
/** @var Config $config */
private $config;
/**
* @param License $license
* @param Config $config
*/
public function __construct(License $license, Config $config)
{
$this->license = $license;
$this->config = $config;
}
/**
* @return EnvironmentConfig
*/
public function createEnvironmentConfig(): EnvironmentConfig
{
$environmentConfig = new EnvironmentConfig(
$this->config->WFdbhost, $this->config->WFdbname, $this->config->WFdbuser,
$this->config->WFdbpass, $this->config->WFdbport, $this->config->WFuserdata,
(array)$this->license->getProperties()
);
return $environmentConfig;
}
}
@@ -0,0 +1,115 @@
<?php
namespace Xentral\Components\Exporter\Collection;
use ArrayObject;
use AppendIterator;
use Iterator;
use IteratorAggregate;
use Xentral\Components\Exporter\Exception\InvalidArgumentException;
final class DataCollection implements Iterator
{
/** @var AppendIterator $data */
private $data;
/**
* @param array|Iterator $data
*
* @throws InvalidArgumentException
*/
public function __construct(...$data)
{
$this->data = new AppendIterator();
foreach ($data as $item) {
$this->append($item);
}
}
/**
* @param array|Iterator|IteratorAggregate $data
*
* @throws InvalidArgumentException
*/
public function append($data)
{
$type = gettype($data);
if ($type === 'object') {
$type = get_class($data);
if ($data instanceof Iterator) {
$type = 'Iterator';
}
if ($data instanceof IteratorAggregate) {
$type = 'IteratorAggregate';
}
}
switch ($type) {
case 'array':
$this->data->append((new ArrayObject($data))->getIterator());
break;
case 'Iterator':
$this->data->append($data);
break;
case 'IteratorAggregate':
$this->data->append($data->getIterator());
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported type "%s".', $type));
break;
}
}
/**
* Return the current element
*
* @return mixed Can return any type.
*/
public function current()
{
return $this->data->current();
}
/**
* Move forward to next element
*
* @return void
*/
public function next()
{
$this->data->next();
}
/**
* Return the key of the current element
*
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
return $this->data->key();
}
/**
* Checks if current position is valid
*
* @return boolean Returns true on success or false on failure.
*/
public function valid()
{
return $this->data->valid();
}
/**
* Rewind the Iterator to the first element
*
* @return void
*/
public function rewind()
{
$this->data->rewind();
}
}
@@ -0,0 +1,120 @@
<?php
namespace Xentral\Components\Exporter\Collection;
use ArrayObject;
use Closure;
use Iterator;
use IteratorAggregate;
use Xentral\Components\Exporter\Exception\InvalidArgumentException;
use Xentral\Components\Exporter\Exception\InvalidReturnTypeException;
final class FormatterCollection implements Iterator
{
/** @var Iterator $data */
private $data;
/** @var callable $callback */
private $callback;
/**
* @param array|Iterator $data
* @param callable|Closure $callback
*
* @throws InvalidArgumentException
*/
public function __construct($data, $callback)
{
if (!is_callable($callback, false)) {
throw new InvalidArgumentException('Callback is not callable');
}
$this->callback = $callback;
$type = gettype($data);
if ($type === 'object') {
$type = get_class($data);
if ($data instanceof Iterator) {
$type = 'Iterator';
}
if ($data instanceof IteratorAggregate) {
$type = 'IteratorAggregate';
}
}
switch ($type) {
case 'array':
$this->data = (new ArrayObject($data))->getIterator();
break;
case 'Iterator':
$this->data = $data;
break;
case 'IteratorAggregate':
$this->data = $data->getIterator();
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported type "%s".', $type));
break;
}
}
/**
* Return the current element
*
* @throws InvalidReturnTypeException
*
* @return mixed Can return any type.
*/
public function current()
{
$result = call_user_func($this->callback, $this->data->current());
if (!is_array($result)) {
throw new InvalidReturnTypeException('Formatter return type is invalid . Callable must return an array.');
}
return $result;
}
/**
* Move forward to next element
*
* @return void
*/
public function next()
{
$this->data->next();
}
/**
* Return the key of the current element
*
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
return $this->data->key();
}
/**
* Checks if current position is valid
*
* @return boolean Returns true on success or false on failure.
*/
public function valid()
{
return $this->data->valid();
}
/**
* Rewind the Iterator to the first element
*
* @return void
*/
public function rewind()
{
$this->data->rewind();
}
}
@@ -0,0 +1,144 @@
<?php
namespace Xentral\Components\Exporter\Csv;
final class CsvConfig
{
/** @var string $delimiter */
private $delimiter;
/** @var string $enclosure */
private $enclosure;
/** @var string $escapeChar */
private $escapeChar;
/** @var string $sourceCharset */
private $sourceCharset;
/** @var string $targetCharset */
private $targetCharset;
/** @var bool $forceEnclosureEnabled */
private $forceEnclosureEnabled;
/**
* @param string $delimiter
* @param string $enclosure
* @param string $escapeChar
* @param string $targetCharset
* @param string $sourceCharset
* @param bool $forceEnclosureEnabled
*/
public function __construct(
$delimiter = ',',
$enclosure = '"',
$escapeChar = "\\",
$targetCharset = 'UTF-8',
$sourceCharset = 'UTF-8',
$forceEnclosureEnabled = false
) {
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->escapeChar = $escapeChar;
$this->targetCharset = $targetCharset;
$this->sourceCharset = $sourceCharset;
$this->forceEnclosureEnabled = $forceEnclosureEnabled;
}
/**
* @return string
*/
public function getDelimiter()
{
return $this->delimiter;
}
/**
* @return string
*/
public function getEnclosure()
{
return $this->enclosure;
}
/**
* @return string
*/
public function getEscapeChar()
{
return $this->escapeChar;
}
/**
* @return string
*/
public function getSourceCharset()
{
return $this->sourceCharset;
}
/**
* @return string
*/
public function getTargetCharset()
{
return $this->targetCharset;
}
/**
* @param string $delimiter
*/
public function setDelimiter($delimiter)
{
$this->delimiter = $delimiter;
}
/**
* @param string $enclosure
*/
public function setEnclosure($enclosure)
{
$this->enclosure = $enclosure;
}
/**
* @param string $escapeChar
*/
public function setEscapeChar($escapeChar)
{
$this->escapeChar = $escapeChar;
}
/**
* @param string $sourceCharset
*/
public function setSourceCharset($sourceCharset)
{
$this->sourceCharset = $sourceCharset;
}
/**
* @param string $targetCharset
*/
public function setTargetCharset($targetCharset)
{
$this->targetCharset = $targetCharset;
}
/**
* @return bool
*/
public function isForceEnclosureEnabled()
{
return $this->forceEnclosureEnabled;
}
/**
* @param bool $forceEnclosureEnabled
*/
public function setForceEnclosureEnabled($forceEnclosureEnabled)
{
$this->forceEnclosureEnabled = $forceEnclosureEnabled;
}
}
@@ -0,0 +1,70 @@
<?php
namespace Xentral\Components\Exporter\Csv;
use Iterator;
use Xentral\Components\Exporter\Exception\FileExistsException;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
final class CsvExporter
{
/** @var CsvConfig $config */
private $config;
/**
* @param CsvConfig|null $config
*/
public function __construct(CsvConfig $config = null)
{
if ($config === null) {
$config = new CsvConfig();
}
$this->config = $config;
}
/**
* @param string $filePath Resource used for writing
* @param array|Iterator $data Multi-dimentional array, Generator or Iterator
*
* @throws FileExistsException|InvalidResourceException
*
* @return void
*/
public function export($filePath, $data)
{
$resource = $this->exportToResource($filePath, $data);
fclose($resource);
}
/**
* Same as ::export() beside that the created resource will be returned
*
* @param string $filePath Resource used for writing
* @param array|Iterator $data Multi-dimentional array, Generator or Iterator
*
* @throws FileExistsException|InvalidResourceException
*
* @return resource
*/
public function exportToResource($filePath, $data)
{
if (is_file($filePath)) {
throw new FileExistsException(sprintf('File creation failed. File "%s" already exists.', $filePath));
}
// 'x+' = Create and open for reading and writing.
// File pointer will be placed at the beginning of the file.
// If the file already exists `fopen` will return false.
// 'b' = Enable binary mode
$resource = @fopen($filePath, 'x+b');
if ($resource === false) {
throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath));
}
$writer = new CsvWriter($resource, $this->config);
$writer->writeLines($data);
return $resource;
}
}
@@ -0,0 +1,147 @@
<?php
namespace Xentral\Components\Exporter\Csv;
use Iterator;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
use Xentral\Components\Exporter\Exception\PhpExtensionMissingException;
final class CsvWriter
{
/** @var resource $handle */
private $handle;
/** @var CsvConfig $config */
private $config;
/**
* @param resource $handle
* @param CsvConfig|null $config
*
* @throws InvalidResourceException If resource is not writable or invalid
* @throws PhpExtensionMissingException If mbstring is missing
*/
public function __construct($handle, CsvConfig $config = null)
{
if (!is_resource($handle)) {
throw new InvalidResourceException('First parameter is not a valid resource.');
}
if (!$this->isStreamWritable($handle)) {
throw new InvalidResourceException('Resource is not writable.');
}
if (!function_exists('mb_convert_encoding')) {
throw new PhpExtensionMissingException('Required PHP extension "mbstring" is missing.');
}
if ($config === null) {
$config = new CsvConfig();
}
$this->config = $config;
$this->handle = $handle;
}
/**
* @param array|Iterator $lines
*
* @return void
*/
public function writeLines($lines)
{
foreach ($lines as $line) {
$this->writeLine($line);
}
}
/**
* @param array $line
*
* @return void
*/
public function writeLine($line)
{
if ($this->config->isForceEnclosureEnabled()) {
$fields = $this->encloseAllValues($line);
fwrite(
$this->handle,
sprintf(
"%s\n",
implode($this->config->getDelimiter(), $this->convertCharset($fields))
)
);
return;
}
fputcsv(
$this->handle,
$this->convertCharset($line),
$this->config->getDelimiter(),
$this->config->getEnclosure(),
$this->config->getEscapeChar()
);
}
/***
* @param array $line
*
* @return array
*/
private function encloseAllValues($line)
{
$escapeChar = $this->config->getEscapeChar();
$enclosure = $this->config->getEnclosure();
$result = [];
foreach ($line as $key => $value) {
$value = str_replace($escapeChar . $enclosure, $enclosure, $value);
$value = str_replace($enclosure, $escapeChar . $enclosure, $value);
$result[$key] = sprintf('%1$s%2$s%1$s', $enclosure, $value);
}
return $result;
}
/**
* @param array $line
*
* @return array
*/
private function convertCharset($line)
{
if ($this->config->getSourceCharset() === $this->config->getTargetCharset()) {
return $line; // No conversion needed
}
$result = [];
foreach ($line as $key => $cellData) {
$result[$key] = mb_convert_encoding(
$cellData,
$this->config->getTargetCharset(),
$this->config->getSourceCharset()
);
}
return $result;
}
/**
* @param resource $handle
*
* @return bool
*/
private function isStreamWritable($handle)
{
$meta = stream_get_meta_data($handle);
$currentMode = $meta['mode'];
$writeModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
foreach ($writeModes as $writeMode) {
if (strpos($currentMode, $writeMode) !== false) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use Xentral\Core\Exception\ComponentExceptionInterface;
interface ExporterExceptionInterface extends ComponentExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use RuntimeException;
class FileExistsException extends RuntimeException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use InvalidArgumentException as SplInvalidArgumentException;
class InvalidArgumentException extends SplInvalidArgumentException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,62 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use Exception;
class InvalidJsonException extends Exception implements ExporterExceptionInterface
{
/**
* @param int $errorCode
*
* @return self
*/
public static function fromJsonError($errorCode)
{
$exception = new self(self::mapJsonError($errorCode));
return $exception;
}
private static function mapJsonError($jsonError)
{
switch ($jsonError) {
case JSON_ERROR_NONE:
$msg = 'Unknown error';
break;
case JSON_ERROR_DEPTH:
$msg = 'The maximum stack depth has been exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = 'Invalid or malformed JSON';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = 'Control character error, possibly incorrectly encoded';
break;
case JSON_ERROR_SYNTAX:
$msg = 'Syntax error';
break;
case JSON_ERROR_UTF8:
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
case JSON_ERROR_RECURSION:
$msg = 'One or more recursive references in the value to be encoded';
break;
case JSON_ERROR_INF_OR_NAN:
$msg = 'One or more NAN or INF values in the value to be encoded';
break;
case JSON_ERROR_UNSUPPORTED_TYPE:
$msg = 'A value of a type that cannot be encoded was given';
break;
case JSON_ERROR_INVALID_PROPERTY_NAME:
$msg = 'A property name that cannot be encoded was given';
break;
case JSON_ERROR_UTF16:
$msg = 'Malformed UTF-16 characters, possibly incorrectly encoded';
break;
default:
$msg = 'Unknown Error';
}
return $msg;
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use RuntimeException;
class InvalidResourceException extends RuntimeException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use LogicException;
class InvalidReturnTypeException extends LogicException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use RuntimeException;
class PhpExtensionMissingException extends RuntimeException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use Exception;
class ResourceWriteException extends Exception implements ExporterExceptionInterface
{
}
@@ -0,0 +1,27 @@
<?php
namespace Xentral\Components\Exporter\Json;
final class JsonConfig
{
/** @var int $options */
private $options;
/**
* @see https://www.php.net/manual/en/json.constants.php
*
* @param int $options
*/
public function __construct($options = 0)
{
$this->options = $options;
}
/**
* @return int
*/
public function getOptions()
{
return $this->options;
}
}
@@ -0,0 +1,69 @@
<?php
namespace Xentral\Components\Exporter\Json;
use Xentral\Components\Exporter\Exception\FileExistsException;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
final class JsonExporter
{
/** @var JsonConfig $config */
private $config;
/**
* @param JsonConfig|null $config
*/
public function __construct(JsonConfig $config = null)
{
if ($config === null) {
$config = new JsonConfig();
}
$this->config = $config;
}
/**
* @param string $filePath Resource used for writing
* @param array $data Multi-dimentional array
*
* @throws \Xentral\Components\Exporter\Exception\InvalidJsonException
* @throws \Xentral\Components\Exporter\Exception\ResourceWriteException
* @return void
*/
public function export($filePath, $data)
{
$resource = $this->exportToResource($filePath, $data);
fclose($resource);
}
/**
* Same as ::export() beside that the created resource will be returned
*
* @param string $filePath Resource used for writing
* @param array $data Multi-dimentional array
*
* @throws \Xentral\Components\Exporter\Exception\InvalidJsonException
* @throws \Xentral\Components\Exporter\Exception\ResourceWriteException
* @return resource
*/
public function exportToResource($filePath, $data)
{
if (is_file($filePath)) {
throw new FileExistsException(sprintf('File creation failed. File "%s" already exists.', $filePath));
}
// 'x+' = Create and open for reading and writing.
// File pointer will be placed at the beginning of the file.
// If the file already exists `fopen` will return false.
// 'b' = Enable binary mode
$resource = @fopen($filePath, 'x+b');
if ($resource === false) {
throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath));
}
$writer = new JsonWriter($resource, $this->config);
$writer->write($data);
return $resource;
}
}
@@ -0,0 +1,90 @@
<?php
namespace Xentral\Components\Exporter\Json;
use JsonSerializable;
use Xentral\Components\Exporter\Exception\InvalidJsonException;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
use Xentral\Components\Exporter\Exception\PhpExtensionMissingException;
use Xentral\Components\Exporter\Exception\ResourceWriteException;
final class JsonWriter
{
/** @var resource $handle */
private $handle;
/** @var JsonConfig $config */
private $config;
/**
* @param resource $handle
* @param JsonConfig|null $config
*
* @throws InvalidResourceException If resource is not writable or invalid
* @throws PhpExtensionMissingException If mbstring is missing
*/
public function __construct($handle, JsonConfig $config = null)
{
if (!is_resource($handle)) {
throw new InvalidResourceException('First parameter is not a valid resource.');
}
if (!$this->isStreamWritable($handle)) {
throw new InvalidResourceException('Resource is not writable.');
}
if (!function_exists('mb_convert_encoding')) {
throw new PhpExtensionMissingException('Required PHP extension "mbstring" is missing.');
}
if ($config === null) {
$config = new JsonConfig();
}
$this->config = $config;
$this->handle = $handle;
}
/**
* @param array|JsonSerializable $data
*
* @throws PhpExtensionMissingException If json is missing
* @throws ResourceWriteException
* @throws InvalidJsonException
*/
public function write($data)
{
if (!function_exists('json_encode')) {
throw new PhpExtensionMissingException('Required PHP extension "json" is missing.');
}
$jsonOptions = $this->config->getOptions();
$jsonString = @json_encode($data, $jsonOptions);
if ($jsonString === false) {
throw InvalidJsonException::fromJsonError(json_last_error());
}
$writeResult = @fwrite($this->handle, $jsonString);
if ($writeResult === false) {
throw new ResourceWriteException("JSON could not be written to resource.");
}
}
/**
* @param resource $handle
*
* @return bool
*/
private function isStreamWritable($handle)
{
$meta = stream_get_meta_data($handle);
$currentMode = $meta['mode'];
$writeModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
foreach ($writeModes as $writeMode) {
if (strpos($currentMode, $writeMode) !== false) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,127 @@
# CSV-Exporter
## Datei erstellen
```php
<?php
use Xentral\Components\Database\Database;
use Xentral\Components\Exporter\Csv\CsvExporter;
/** @var Database $db */
$db = $container->get('Database');
$data = $db->yieldAll('SELECT e.* FROM employees AS e');
$filePath = tempnam(sys_get_temp_dir(), 'employees') . '.csv';
$exporter = new CsvExporter();
$exporter->export($filePath, $data);
```
##### Rückgabewerte
* Die `export()` Methode liefert nichts zurück.
* Die `exportToResource()` gibt die geöffnete Ressource zurück. Die Ressource muss manuell (mit `fclose()`)
geschlossen werden. Der Dateizeiger ist auf `EOF` platziert.
##### Funktionsparameter
Beide `export*()`-Methoden nehmen die gleichen Parameter entgegen.
* Als erster Parameter wird ein absoluter Dateipfad erwartet. Alternativ kann ein Stream Wrapper angegeben werden.
Beispiele:
* `php://output` um die CSV direkt auszugeben (`echo`).
* `php://memory` um in den Arbeitsspeicher zu schreiben.
* `php://temp` um in eine temporäre Datei zu schreiben.
* Der zweite Parameter nimmt die Daten entgegen die als CSV exportiert werden sollen. Folgende Typen sind erlaubt:
* `array`
* `Generator`
* `Iterator`
* `IteratorAggregate`
##### Konstruktor-Parameter
* Der erste Konstruktor-Parameter ist optional und nimmt die CSV-Konfiguration entgegen;
siehe _Erweiterte Beispiele_ > _CSV konfigurieren_.
## Datei-Download erstellen
```php
<?php
use Xentral\Components\Database\Database;
use Xentral\Components\Exporter\Csv\CsvExporter;
/** @var Database $db */
$db = $container->get('Database');
$data = $db->yieldAll('SELECT e.* FROM employees AS e');
$exporter = new CsvExporter();
$resource = $exporter->exportToResource('php://memory', $data);
rewind($resource);
$stat = fstat($resource);
header('Cache-Control: must-revalidate');
header('Pragma: must-revalidate');
header('Content-type: text/csv');
header('Content-Disposition: attachment; filename="employees.csv"');
header('Content-Length: ' . $stat['size']);
fpassthru($resource);
fclose($resource);
```
## Erweiterte Beispiele
### CSV konfigurieren
```php
<?php
use Xentral\Components\Exporter\Csv\CsvConfig;
use Xentral\Components\Exporter\Csv\CsvExporter;
$csvConfig = new CsvConfig();
$csvConfig->setDelimiter(';');
$csvConfig->setEnclosure('"');
$csvConfig->setSourceCharset('UTF-8');
$csvConfig->setTargetCharset('ISO-8859-1');
$exporter = new CsvExporter($csvConfig);
$exporter->export($filePath, $data);
```
### Daten zusammenführen
```php
<?php
use Xentral\Components\Exporter\Csv\CsvExporter;
use Xentral\Components\Exporter\Collection\DataCollection;
$collection = new DataCollection($headline, $employees);
$exporter = new CsvExporter();
$exporter->export($filePath, $collection);
```
### Daten formatieren
```php
<?php
use Xentral\Components\Exporter\Csv\CsvExporter;
use Xentral\Components\Exporter\Collection\FormatterCollection;
$formatter = new FormatterCollection($data, function ($row) {
$row['fullname'] = $row['firstname'] . ' ' . $row['lastname'];
return $row;
});
$exporter = new CsvExporter();
$exporter->export($filePath, $formatter);
```
Statt einer anonymen Funktion (`Closure`) kann ein `callable` übergeben werden.
@@ -0,0 +1,96 @@
<?php
namespace Xentral\Components\Filesystem\Adapter;
interface AdapterInterface extends ReaderAdapterInterface
{
/**
* Creates a new file
*
* @param string $path
* @param string $contents
* @param array $config
*
* @return bool
*/
public function write($path, $contents, array $config = []);
/**
* Creates a new file using a stream
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @return bool
*/
public function writeStream($path, $resource, array $config = []);
/**
* Updates an existing file
*
* @param string $path
* @param string $contents
* @param array $config
*
* @return bool
*/
public function update($path, $contents, array $config = []);
/**
* Updates an existing file using a stream
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @return bool
*/
public function updateStream($path, $resource, array $config = []);
/**
* Renames a file
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function rename($path, $newpath);
/**
* Copies a file to new location
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function copy($path, $newpath);
/**
* Deletes a single file
*
* @param string $path
*
* @return bool
*/
public function delete($path);
/**
* Deletes a directory and its contents
*
* @param string $directory
*
* @return bool
*/
public function deleteDir($directory);
/**
* @param string $directory
* @param array $config
*
* @return bool
*/
public function createDir($directory, array $config = []);
}
@@ -0,0 +1,157 @@
<?php
namespace Xentral\Components\Filesystem\Adapter;
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
final class FtpConfig
{
/** @var string $hostname */
private $hostname;
/** @var string $username */
private $username;
/** @var string $password */
private $password;
/** @var string $rootDir */
private $rootDir;
/** @var int $port */
private $port;
/** @var int $timeout */
private $timeout;
/** @var bool $passive */
private $passive;
/** @var bool $ssl */
private $ssl;
/**
* @param string $hostname
* @param string $username
* @param string $password
* @param string $rootDir
* @param int $port
* @param int $timeout
* @param bool $passive
* @param bool $ssl
*/
public function __construct(
$hostname,
$username,
$password,
$rootDir = '/',
$port = 21,
$timeout = 30,
$passive = true,
$ssl = false
) {
if (empty($hostname)) {
throw new InvalidArgumentException('Hostname is empty.');
}
if (empty($username)) {
throw new InvalidArgumentException('Username is empty.');
}
if (empty($password)) {
throw new InvalidArgumentException('Password is empty.');
}
if (empty($rootDir)) {
throw new InvalidArgumentException('Root dir is empty.');
}
$this->hostname = (string)$hostname;
$this->username = (string)$username;
$this->password = (string)$password;
$this->rootDir = (string)$rootDir;
$this->port = (int)$port;
$this->timeout = (int)$timeout;
$this->passive = (bool)$passive;
$this->ssl = (bool)$ssl;
}
/**
* @return array
*/
public function toArray()
{
return [
'host' => $this->hostname,
'username' => $this->username,
'password' => $this->password,
'root' => $this->rootDir,
'port' => $this->port,
'timeout' => $this->timeout,
'passive' => $this->passive,
'ssl' => $this->ssl,
'recurseManually' => true,
];
}
/**
* @return string
*/
public function getHostname()
{
return $this->hostname;
}
/**
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* @return null
*/
public function getRootDir()
{
return $this->rootDir;
}
/**
* @return int
*/
public function getPort()
{
return $this->port;
}
/**
* @return int
*/
public function getTimeout()
{
return $this->timeout;
}
/**
* @return bool
*/
public function isPassive()
{
return $this->passive;
}
/**
* @return bool
*/
public function isSsl()
{
return $this->ssl;
}
}
@@ -0,0 +1,355 @@
<?php
namespace Xentral\Components\Filesystem\Adapter;
use League\Flysystem\AdapterInterface as LeagueAdapterInterface;
use League\Flysystem\Config as LeagueConfig;
use League\Flysystem\Util;
use League\Flysystem\Util\ContentListingFormatter;
use Xentral\Components\Filesystem\PathInfo;
final class LeagueAdapterWrapper implements AdapterInterface
{
/** @var LeagueAdapterInterface $league */
private $league;
/** @var bool $caseSensitive */
private $caseSensitive = true;
/**
* @param LeagueAdapterInterface $league
*/
public function __construct(LeagueAdapterInterface $league)
{
$this->league = $league;
}
/**
* @param string $path
*
* @return bool
*/
public function has($path)
{
$path = $this->normalizePath($path);
return (bool)$this->league->has($path) !== false;
}
/**
* @param string $path
*
* @return PathInfo|false
*/
public function getInfo($path)
{
$path = $this->normalizePath($path);
$metainfo = $this->league->getMetadata($path);
if (!$metainfo) {
return false;
}
$directory = Util::dirname($path);
$metainfo['path'] = $path;
$formatter = new ContentListingFormatter($directory, false, $this->caseSensitive);
$contents = $formatter->formatListing([$metainfo]);
if (count($contents) !== 1) {
return false;
}
return new PathInfo($contents[0]);
}
/**
* @param string $path
*
* @return string|false
*/
public function read($path)
{
$path = $this->normalizePath($path);
$meta = $this->league->getMetadata($path);
if ($meta['type'] === self::TYPE_DIR) {
return false;
}
$result = $this->league->read($path);
if (!$result || !isset($result['contents'])) {
return false;
}
return $result['contents'];
}
/**
* @param string $path
*
* @return resource|false
*/
public function readStream($path)
{
$path = $this->normalizePath($path);
$meta = $this->league->getMetadata($path);
if ($meta['type'] === self::TYPE_DIR) {
return false;
}
$result = $this->league->readStream($path);
if (!$result || !isset($result['stream'])) {
return false;
}
return $result['stream'];
}
/**
* @param string $directory
* @param bool $recursive
*
* @return array
*/
public function listContents($directory = '', $recursive = false)
{
$directory = $this->normalizePath($directory);
$contents = $this->getLeagueAdapter()->listContents($directory, $recursive);
$formatter = new ContentListingFormatter($directory, $recursive, $this->caseSensitive);
return $formatter->formatListing($contents);
}
/**
* @param string $path
*
* @return array|false
*/
public function getMetadata($path)
{
$path = $this->normalizePath($path);
return $this->league->getMetadata($path);
}
/**
* @param string $path
*
* @return string|false
*/
public function getType($path)
{
$path = $this->normalizePath($path);
$meta = $this->league->getMetadata($path);
if ($meta === false || !isset($meta['type'])) {
return false;
}
return $meta['type'];
}
/**
* @param string $path
*
* @return int|false
*/
public function getSize($path)
{
$path = $this->normalizePath($path);
$meta = $this->league->getSize($path);
if ($meta === false || !isset($meta['size'])) {
return false;
}
return (int)$meta['size'];
}
/**
* @param string $path
*
* @return int|false
*/
public function getTimestamp($path)
{
$path = $this->normalizePath($path);
$meta = $this->league->getTimestamp($path);
if ($meta === false || !isset($meta['timestamp'])) {
return false;
}
return (int)$meta['timestamp'];
}
/**
* @param string $path
*
* @return string|false
*/
public function getMimetype($path)
{
$path = $this->normalizePath($path);
$meta = $this->league->getMimetype($path);
if ($meta === false || !isset($meta['mimetype'])) {
return false;
}
if ($meta['type'] === 'dir') {
return 'directory';
}
return $meta['mimetype'];
}
/**
* Creates a new file
*
* @param string $path
* @param string $contents
* @param array $config
*
* @return bool
*/
public function write($path, $contents, array $config = [])
{
$path = $this->normalizePath($path);
return $this->league->write($path, $contents, new LeagueConfig($config)) !== false;
}
/**
* Creates a new file using a stream
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @return bool
*/
public function writeStream($path, $resource, array $config = [])
{
$path = $this->normalizePath($path);
return $this->league->writeStream($path, $resource, new LeagueConfig($config)) !== false;
}
/**
* Updates an existing file
*
* @param string $path
* @param string $contents
* @param array $config
*
* @return bool
*/
public function update($path, $contents, array $config = [])
{
$path = $this->normalizePath($path);
return $this->league->update($path, $contents, new LeagueConfig($config)) !== false;
}
/**
* Updates an existing file using a stream
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @return bool
*/
public function updateStream($path, $resource, array $config = [])
{
$path = $this->normalizePath($path);
return $this->league->updateStream($path, $resource, new LeagueConfig($config)) !== false;
}
/**
* Renames a file
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function rename($path, $newpath)
{
$path = $this->normalizePath($path);
$newpath = $this->normalizePath($newpath);
return $this->league->rename($path, $newpath);
}
/**
* Copies a file to new location
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function copy($path, $newpath)
{
$path = $this->normalizePath($path);
$newpath = $this->normalizePath($newpath);
return $this->league->copy($path, $newpath);
}
/**
* Deletes a single file
*
* @param string $path
*
* @return bool
*/
public function delete($path)
{
$path = $this->normalizePath($path);
return $this->league->delete($path);
}
/**
* Deletes a directory and its contents
*
* @param string $directory
*
* @return bool
*/
public function deleteDir($directory)
{
$directory = $this->normalizePath($directory);
return $this->league->deleteDir($directory);
}
/**
* @param string $directory
* @param array $config
*
* @return bool
*/
public function createDir($directory, array $config = [])
{
$directory = $this->normalizePath($directory);
return $this->league->createDir($directory, new LeagueConfig($config)) !== false;
}
/**
* @return LeagueAdapterInterface
*/
public function getLeagueAdapter()
{
return $this->league;
}
/**
* @param string $path
*
* @return string
*/
public function normalizePath($path)
{
return Util::normalizePath($path);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Xentral\Components\Filesystem\Adapter;
use Xentral\Components\Filesystem\PathInfo;
interface ReaderAdapterInterface
{
const TYPE_DIR = 'dir';
const TYPE_FILE = 'file';
/**
* @param string $path
*
* @return bool
*/
public function has($path);
/**
* @param string $path
*
* @return PathInfo|false
*/
public function getInfo($path);
/**
* @param string $path
*
* @return string|false
*/
public function read($path);
/**
* @param string $path
*
* @return resource|false
*/
public function readStream($path);
/**
* @param string $directory
* @param bool $recursive
*
* @return array
*/
public function listContents($directory = '', $recursive = false);
/**
* @param string $path
*
* @return string|false [dir|file]
*/
public function getType($path);
/**
* @param string $path
*
* @return array|false
*/
public function getMetadata($path);
/**
* @param string $path
*
* @return int|false
*/
public function getSize($path);
/**
* @param string $path
*
* @return int|false
*/
public function getTimestamp($path);
/**
* @param string $path
*
* @return string|false
*/
public function getMimetype($path);
}
@@ -0,0 +1,28 @@
<?php
namespace Xentral\Components\Filesystem;
use Xentral\Core\DependencyInjection\ContainerInterface;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices()
{
return [
'FilesystemFactory' => 'onInitFilesystemFactory',
];
}
/**
* @param ContainerInterface $container
*
* @return FilesystemFactory
*/
public static function onInitFilesystemFactory(ContainerInterface $container)
{
return new FilesystemFactory($container->get('Database'));
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Exception;
use RuntimeException;
class DirNotFoundException extends RuntimeException implements FilesystemExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Exception;
use RuntimeException;
class FileExistsException extends RuntimeException implements FilesystemExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Exception;
use RuntimeException;
class FileNotFoundException extends RuntimeException implements FilesystemExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Exception;
use RuntimeException;
class FilesystemException extends RuntimeException implements FilesystemExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Exception;
use Xentral\Core\Exception\ComponentExceptionInterface;
interface FilesystemExceptionInterface extends ComponentExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Exception;
use InvalidArgumentException as SplInvalidArgumentException;
class InvalidArgumentException extends SplInvalidArgumentException implements FilesystemExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Exception;
use LogicException;
class RootViolationException extends LogicException implements FilesystemExceptionInterface
{
}
@@ -0,0 +1,429 @@
<?php
namespace Xentral\Components\Filesystem;
use Xentral\Components\Filesystem\Adapter\AdapterInterface;
use Xentral\Components\Filesystem\Exception\DirNotFoundException;
use Xentral\Components\Filesystem\Exception\FileExistsException;
use Xentral\Components\Filesystem\Exception\FileNotFoundException;
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
use Xentral\Components\Filesystem\Exception\RootViolationException;
final class Filesystem implements FilesystemInterface
{
/** @var AdapterInterface $adapter */
private $adapter;
/**
* @param AdapterInterface $adapter
*/
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
/**
* Checks if file or directory exists
*
* @param string $path
*
* @return bool
*/
public function has($path)
{
return $this->adapter->has($path);
}
/**
* @param string $path
*
* @throws FileNotFoundException
*
* @return PathInfo|false
*/
public function getInfo($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->getInfo($path);
}
/**
* List directory contents
*
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listContents($directory = '', $recursive = false)
{
$result = [];
$contents = $this->adapter->listContents($directory, $recursive);
foreach ($contents as $metainfo) {
$result[] = PathInfo::fromMeta($metainfo);
}
return $result;
}
/**
* Lists only directories
*
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listDirs($directory = '', $recursive = false)
{
$result = [];
$contents = $this->adapter->listContents($directory, $recursive);
foreach ($contents as $metainfo) {
if ($metainfo['type'] === 'dir') {
$result[] = PathInfo::fromMeta($metainfo);
}
}
return $result;
}
/**
* Lists only files
*
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listFiles($directory = '', $recursive = false)
{
$result = [];
$contents = $this->adapter->listContents($directory, $recursive);
foreach ($contents as $metainfo) {
if ($metainfo['type'] === 'file') {
$result[] = PathInfo::fromMeta($metainfo);
}
}
return $result;
}
/**
* List only paths as strings
*
* @param string $directory
* @param bool $recursive
*
* @return array|string[]
*/
public function listPaths($directory = '', $recursive = false)
{
$contents = $this->adapter->listContents($directory, $recursive);
return array_column($contents, 'path');
}
/**
* @param string $path
*
* @throws FileNotFoundException
*
* @return string [dir|file]
*/
public function getType($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->getType($path);
}
/**
* Gets the filesize
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return int|false
*/
public function getSize($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->getSize($path);
}
/**
* Gets the timestamp from last update
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return string|false
*/
public function getTimestamp($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->getTimestamp($path);
}
/**
* @param string $path
*
* @throws FileNotFoundException
*
* @return string|false
*/
public function getMimetype($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->getMimetype($path);
}
/**
* Read file content
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return string|false
*/
public function read($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->read($path);
}
/**
* Read file content
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return resource|false
*/
public function readStream($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->readStream($path);
}
/**
* Writes to a new file
*
* @param string $path
* @param string $contents
* @param array $config
*
* @throws FileExistsException
*
* @return bool
*/
public function write($path, $contents, array $config = [])
{
if ($this->has($path)) {
throw new FileExistsException(sprintf('File "%s" exists already.', $path));
}
return $this->adapter->write($path, $contents, $config);
}
/**
* Writes to a new file
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @throws InvalidArgumentException
* @throws FileExistsException
*
* @return bool
*/
public function writeStream($path, $resource, array $config = [])
{
if ($this->has($path)) {
throw new FileExistsException(sprintf('File "%s" exists already.', $path));
}
if (!is_resource($resource)) {
throw new InvalidArgumentException('Second parameter must be a resource.');
}
return $this->adapter->writeStream($path, $resource, $config);
}
/**
* Creates a file or updates the file contents
*
* @param string $path
* @param string $contents
* @param array $config
*
* @return bool
*/
public function put($path, $contents, array $config = [])
{
if (!$this->has($path)) {
return $this->adapter->write($path, $contents, $config);
}
return $this->adapter->update($path, $contents, $config);
}
/**
* Creates a file or updates the file contents
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @throws InvalidArgumentException
*
* @return bool
*/
public function putStream($path, $resource, array $config = [])
{
if (!is_resource($resource)) {
throw new InvalidArgumentException('Second parameter must be a resource.');
}
if (!$this->has($path)) {
return $this->adapter->writeStream($path, $resource, $config);
}
return $this->adapter->updateStream($path, $resource, $config);
}
/**
* Renames a file
*
* @param string $path
* @param string $newpath
*
* @throws FileExistsException
* @throws FileNotFoundException
*
* @return bool
*/
public function rename($path, $newpath)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
if ($this->has($newpath)) {
throw new FileExistsException(sprintf('File "%s" exists already.', $newpath));
}
return $this->adapter->rename($path, $newpath);
}
/**
* Copies a file to new location
*
* @param string $path
* @param string $newpath
*
* @throws FileExistsException
* @throws FileNotFoundException
*
* @return bool
*/
public function copy($path, $newpath)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
if ($this->has($newpath)) {
throw new FileExistsException(sprintf('File "%s" exists already.', $newpath));
}
return $this->adapter->copy($path, $newpath);
}
/**
* Deletes a single file
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return bool
*/
public function delete($path)
{
if (!$this->has($path)) {
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
}
return $this->adapter->delete($path);
}
/**
* Deletes a directory and all its contents
*
* @param string $dirname
*
* @throws DirNotFoundException
* @throws RootViolationException
*
* @return bool
*/
public function deleteDir($dirname)
{
$info = $this->adapter->getInfo($dirname);
if (!$info) {
throw new DirNotFoundException(sprintf('Directory "%s" not found.', $dirname));
}
if ($info->getPath() === '') {
throw new RootViolationException('Root directory can not be deleted.');
}
return $this->adapter->deleteDir($dirname);
}
/**
* Creates a directory
*
* @param string $dirname
* @param array $config
*
* @return bool
*/
public function createDir($dirname, array $config = [])
{
return $this->adapter->createDir($dirname, $config);
}
/**
* @return AdapterInterface
*/
public function getAdapter()
{
return $this->adapter;
}
}
@@ -0,0 +1,87 @@
<?php
namespace Xentral\Components\Filesystem;
use Exception;
use Xentral\Components\Database\Database;
use Xentral\Components\Filesystem\Adapter\FtpConfig;
use Xentral\Components\Filesystem\Adapter\LeagueAdapterWrapper;
use Xentral\Components\Filesystem\Exception\FilesystemException;
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
use Xentral\Components\Filesystem\Flysystem\FtpAdapterDecorator;
use Xentral\Components\Filesystem\Flysystem\LocalAdapterDecorator;
final class FilesystemFactory
{
/** @var Database $db */
private $db;
/**
* @param Database $database
*/
public function __construct(Database $database)
{
$this->db = $database;
}
/**
* @param string $root Absolute path
* @param array $config
*
* @return FilesystemInterface
*/
public function createLocal($root, array $config = [])
{
try {
$writeFlags = isset($config['write_flags']) ? $config['write_flags'] : LOCK_EX;
$linkHandling = isset($config['link_handling']) ? $config['link_handling'] : LocalAdapterDecorator::SKIP_LINKS;
$permissions = isset($config['permissions']) ? $config['permissions'] : [];
$leagueLocalAdapter = new LocalAdapterDecorator($root, $writeFlags, $linkHandling, $permissions);
$leagueAdapterWrapper = new LeagueAdapterWrapper($leagueLocalAdapter);
return new Filesystem($leagueAdapterWrapper);
//
} catch (Exception $e) {
throw new FilesystemException($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* @param FtpConfig $config
*
* @return FilesystemInterface
*/
public function createFtp(FtpConfig $config)
{
try {
$leagueFtpAdapter = new FtpAdapterDecorator($config->toArray());
$leagueAdapterWrapper = new LeagueAdapterWrapper($leagueFtpAdapter);
return new Filesystem($leagueAdapterWrapper);
//
} catch (Exception $e) {
throw new FilesystemException($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* @param FilesystemInterface $filesystem
* @param int $syncId
*
* @return FilesystemSyncCache
*/
public function createSync(FilesystemInterface $filesystem, $syncId)
{
try {
if (get_class($filesystem) === FilesystemSyncCache::class) {
throw new InvalidArgumentException('FilesystemSyncWrapper can not wrap it self.');
}
return new FilesystemSyncCache($this->db, $filesystem, $syncId);
//
} catch (Exception $e) {
throw new FilesystemException($e->getMessage(), (int)$e->getCode(), $e);
}
}
}
@@ -0,0 +1,256 @@
<?php
namespace Xentral\Components\Filesystem;
use Xentral\Components\Filesystem\Adapter\AdapterInterface;
use Xentral\Components\Filesystem\Exception\DirNotFoundException;
use Xentral\Components\Filesystem\Exception\FileExistsException;
use Xentral\Components\Filesystem\Exception\FileNotFoundException;
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
use Xentral\Components\Filesystem\Exception\RootViolationException;
interface FilesystemInterface
{
/**
* Checks if file or directory exists
*
* @param string $path
*
* @return bool
*/
public function has($path);
/**
* @param string $path
*
* @throws FileNotFoundException
*
* @return PathInfo|false
*/
public function getInfo($path);
/**
* List directory contents
*
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listContents($directory = '', $recursive = false);
// public function filterContents(array $filter = [], $directory = '', $recursive = false); // @todo
//
// // @todo ExtendedLocal
// public function isReadable();
// public function isWriteable();
// public function getOwner();
// public function getGroup();
/**
* Lists only directories
*
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listDirs($directory = '', $recursive = false);
/**
* Lists only files
*
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listFiles($directory = '', $recursive = false);
/**
* List only paths as strings
*
* @param string $directory
* @param bool $recursive
*
* @return array|string[]
*/
public function listPaths($directory = '', $recursive = false);
/**
* @param string $path
*
* @throws FileNotFoundException
*
* @return string [dir|file]
*/
public function getType($path);
/**
* Gets the filesize
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return int|false
*/
public function getSize($path);
/**
* Gets the timestamp from last update
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return string|false
*/
public function getTimestamp($path);
/**
* @param string $path
*
* @throws FileNotFoundException
*
* @return string|false
*/
public function getMimetype($path);
/**
* Read file content
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return string|false
*/
public function read($path);
/**
* Read file content
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return resource|false
*/
public function readStream($path);
/**
* Writes to a new file
*
* @param string $path
* @param string $contents
* @param array $config
*
* @throws FileExistsException
*
* @return bool
*/
public function write($path, $contents, array $config = []);
/**
* Writes to a new file
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @throws InvalidArgumentException
* @throws FileExistsException
*
* @return bool
*/
public function writeStream($path, $resource, array $config = []);
/**
* Creates a file or updates the file contents
*
* @param string $path
* @param string $contents
* @param array $config
*
* @return bool
*/
public function put($path, $contents, array $config = []);
/**
* Creates a file or updates the file contents
*
* @param string $path
* @param resource $resource
* @param array $config
*
* @throws InvalidArgumentException
*
* @return bool
*/
public function putStream($path, $resource, array $config = []);
/**
* Renames a file
*
* @param string $path
* @param string $newpath
*
* @throws FileExistsException
* @throws FileNotFoundException
*
* @return bool
*/
public function rename($path, $newpath);
/**
* Copies a file to new location
*
* @param string $path
* @param string $newpath
*
* @throws FileExistsException
* @throws FileNotFoundException
*
* @return bool
*/
public function copy($path, $newpath);
/**
* Deletes a single file
*
* @param string $path
*
* @throws FileNotFoundException
*
* @return bool
*/
public function delete($path);
/**
* Deletes a directory and all its contents
*
* @param string $dirname
*
* @throws DirNotFoundException
* @throws RootViolationException
*
* @return bool
*/
public function deleteDir($dirname);
/**
* Creates a directory
*
* @param string $dirname
* @param array $config
*
* @return bool
*/
public function createDir($dirname, array $config = []);
/**
* @return AdapterInterface
*/
public function getAdapter();
}
@@ -0,0 +1,446 @@
<?php
namespace Xentral\Components\Filesystem;
use Xentral\Components\Database\Database;
use Xentral\Components\Filesystem\Exception\FileNotFoundException;
use Xentral\Components\Filesystem\Exception\FilesystemException;
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
/**
* @todo Datenbank anlegen
*/
final class FilesystemSyncCache implements FilesystemInterface
{
/** @var Database $db */
private $db;
/** @var FilesystemInterface $fs */
private $fs;
/** @var int $syncId */
private $syncId;
/**
* @param Database $database
* @param FilesystemInterface $filesystem
* @param int $syncId
*/
public function __construct(Database $database, FilesystemInterface $filesystem, $syncId)
{
if ((int)$syncId <= 0) {
throw new InvalidArgumentException(sprintf('Sync-ID "%s" is invalid.', $syncId));
}
$this->db = $database;
$this->fs = $filesystem;
$this->syncId = (int)$syncId;
}
/**
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listChanges($directory = '', $recursive = false)
{
$result = $this->fs->listContents($directory, $recursive);
$location = PathUtil::normalizePath($directory);
$cache = $this->readCache($location, $recursive);
foreach ($result as $item) {
$path = $item->getPath();
// Ignore directories; only files are nessesary for syncing
if ($item->isDir()) {
continue;
}
// Defaults
$item->set('missing', false);
$item->set('modified', null);
if (!array_key_exists($path, $cache)) {
$item->set('missing', true);
continue;
}
if ((int)$cache[$path]['size'] !== (int)$item->getSize()) {
$item->set('modified', true);
continue;
}
if ((int)$cache[$path]['timestamp'] !== (int)$item->getTimestamp()) {
$item->set('modified', true);
continue;
}
$item->set('modified', false);
}
return $result;
}
/**
* Lists deleted files
*
* Lists files that are present in cache but does not exist on the filesystem any more.
*
* @param string $directory
* @param bool $recursive
*
* @return array|PathInfo[]
*/
public function listDeleted($directory = '', $recursive = false)
{
$paths = $this->fs->listPaths($directory, $recursive);
$location = PathUtil::normalizePath($directory);
$cache = $this->readCache($location, $recursive);
foreach ($cache as $path => $cacheItem) {
if (in_array($path, $paths, true)) {
unset($cache[$path]);
}
}
$result = [];
foreach ($cache as $cacheItem) {
$pathinfo = PathUtil::pathinfo($cacheItem['path']);
$result[] = new PathInfo(array_merge($cacheItem, $pathinfo));
}
return $result;
}
/**
* @inheritdoc
*/
public function has($path)
{
return $this->fs->has($path);
}
/**
* @inheritdoc
*/
public function getInfo($path)
{
return $this->fs->getInfo($path);
}
/**
* @inheritdoc
*/
public function getType($path)
{
return $this->fs->getType($path);
}
/**
* @inheritdoc
*/
public function getSize($path)
{
return $this->fs->getSize($path);
}
/**
* @inheritdoc
*/
public function getTimestamp($path)
{
return $this->fs->getTimestamp($path);
}
/**
* @inheritdoc
*/
public function getMimetype($path)
{
return $this->fs->getMimetype($path);
}
/**
* @inheritdoc
*/
public function listContents($directory = '', $recursive = false)
{
return $this->fs->listContents($directory, $recursive);
}
/**
* @inheritdoc
*/
public function listDirs($directory = '', $recursive = false)
{
return $this->fs->listDirs($directory, $recursive);
}
/**
* @inheritdoc
*/
public function listFiles($directory = '', $recursive = false)
{
return $this->fs->listFiles($directory, $recursive);
}
/**
* @inheritdoc
*/
public function listPaths($directory = '', $recursive = false)
{
return $this->fs->listPaths($directory, $recursive);
}
/**
* @inheritdoc
*/
public function read($path)
{
$result = $this->fs->read($path);
$this->updateCachePath($path);
return $result;
}
/**
* @inheritdoc
*/
public function readStream($path)
{
$result = $this->fs->readStream($path);
$this->updateCachePath($path);
return $result;
}
/**
* @inheritdoc
*/
public function write($path, $contents, array $config = [])
{
$result = $this->fs->write($path, $contents, $config);
$this->updateCachePath($path);
return $result;
}
/**
* @inheritdoc
*/
public function writeStream($path, $resource, array $config = [])
{
$result = $this->fs->writeStream($path, $resource, $config);
$this->updateCachePath($path);
return $result;
}
/**
* @inheritdoc
*/
public function put($path, $contents, array $config = [])
{
$result = $this->fs->put($path, $contents, $config);
$this->updateCachePath($path);
return $result;
}
/**
* @inheritdoc
*/
public function putStream($path, $resource, array $config = [])
{
$result = $this->fs->putStream($path, $resource, $config);
$this->updateCachePath($path);
return $result;
}
/**
* @inheritdoc
*/
public function delete($path)
{
$result = $this->fs->delete($path);
$this->deleteCachePath($path);
return $result;
}
/**
* Deletes a single file, but without Exception if path does not exist.
*
* @param string $path
*
* @return bool
*/
public function softDelete($path)
{
$result = false;
try {
$this->deleteCachePath($path);
$result = $this->fs->delete($path);
} catch (FileNotFoundException $e) {
// nope - its soft
}
return $result;
}
/**
* @inheritdoc
*/
public function deleteDir($dirname)
{
$result = $this->fs->deleteDir($dirname);
$this->deleteCacheDir($dirname);
return $result;
}
/**
* @inheritdoc
*/
public function createDir($dirname, array $config = [])
{
return $this->fs->createDir($dirname, $config);
}
/**
* @inheritdoc
*/
public function rename($path, $newpath)
{
$result = $this->fs->rename($path, $newpath);
$this->deleteCachePath($path);
return $result;
}
/**
* @inheritdoc
*/
public function copy($path, $newpath)
{
$result = $this->fs->copy($path, $newpath);
$this->updateCachePath($newpath);
return $result;
}
/**
* @inheritdoc
*/
public function getAdapter()
{
return $this->fs->getAdapter();
}
/**
* @param string $path
* @param bool $recursive
*
* @return array
*/
private function readCache($path, $recursive = false)
{
$this->ensureDependencies();
$path = $this->normalizePath($path);
return $this->db->fetchAssoc(
'SELECT f.path, f.dirname, f.type, f.size, f.updated_at AS timestamp ' .
'FROM sync_files AS f ' .
'WHERE f.sync_id = :sync_id AND f.dirname LIKE :path_prefix',
[
'sync_id' => $this->syncId,
'path_prefix' => $recursive === true ? $path . '%' : $path,
]
);
}
/**
* @param string $path
*
* @return void
*/
private function updateCachePath($path)
{
$this->ensureDependencies();
$info = $this->fs->getInfo($path);
$this->db->perform(
'REPLACE INTO sync_files (sync_id, `path`, dirname, type, size, updated_at) ' .
'VALUES (:sync_id, :path, :dirname, :type, :size, :updated_at)',
[
'sync_id' => $this->syncId,
'path' => $info->getPath(),
'dirname' => $info->getDir(),
'type' => $info->getType(),
'size' => (int)$info->getSize(),
'updated_at' => (int)$info->getTimestamp(),
]
);
}
/**
* @param string $path
*
* @return int Deleted row count
*/
private function deleteCachePath($path)
{
$this->ensureDependencies();
$path = $this->normalizePath($path);
return $this->db->fetchAffected(
'DELETE FROM sync_files WHERE sync_id = :sync_id AND `path` = :path',
['sync_id' => $this->syncId, 'path' => $path]
);
}
/**
* @param string $dirname
*
* @return int Deleted row count
*/
private function deleteCacheDir($dirname)
{
$this->ensureDependencies();
$dirname = $this->normalizePath($dirname);
return $this->db->fetchAffected(
'DELETE FROM sync_files WHERE sync_id = :sync_id AND `dirname` LIKE :dirname',
['sync_id' => $this->syncId, 'dirname' => $dirname . '%']
);
}
/**
* @param string $path
*
* @return string
*/
private function normalizePath($path)
{
return PathUtil::normalizePath($path);
}
/**
* @throws FilesystemException
*
* @return void
*/
private function ensureDependencies()
{
if ($this->db === null || $this->syncId === null) {
throw new FilesystemException('Can not continue. Required dependencies are missing.');
}
}
}
@@ -0,0 +1,52 @@
<?php
namespace Xentral\Components\Filesystem\Flysystem;
use League\Flysystem\Adapter\Ftp;
final class FtpAdapterDecorator extends Ftp
{
/**
* @inheritdoc
*/
public function getMetadata($path)
{
$metadata = parent::getMetadata($path);
if ($metadata === false) {
return false; // File does not exist
}
if ($metadata['type'] === 'dir') {
$metadata['timestamp'] = null; // ftp_mdtm() does not work with directories.
return $metadata;
}
if ($metadata['timestamp'] === null) {
$data = $this->getTimestamp($path);
$metadata['timestamp'] = $data !== false && isset($data['timestamp']) ? $data['timestamp'] : null;
}
return $metadata;
}
/**
* @inheritdoc
*/
protected function normalizeListing(array $listing, $prefix = '')
{
$result = parent::normalizeListing($listing, $prefix);
foreach ($result as &$item) {
if ($item['type'] === 'dir') {
$item['timestamp'] = null; // ftp_mdtm() does not work with directories.
continue;
}
if (!isset($item['timestamp'])) {
$data = $this->getTimestamp($item['path']);
$item['timestamp'] = $data !== false && isset($data['timestamp']) ? $data['timestamp'] : null;
}
}
return $result;
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem\Flysystem;
use League\Flysystem\Adapter\Local;
final class LocalAdapterDecorator extends Local
{
}
+221
View File
@@ -0,0 +1,221 @@
<?php
namespace Xentral\Components\Filesystem;
use League\Flysystem\Util;
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
final class PathInfo
{
const TYPE_FILE = 'file';
const TYPE_DIR = 'dir';
/** @var array $readonlyProperties */
private static $readonlyProperties = [
'type',
'path',
'dirname',
'filename',
'basename',
'extension',
'timestamp',
'size',
];
/** @var array $data */
private $data;
/**
* @param array $data
*/
public function __construct(array $data)
{
// required
$data['type'] = (string)$data['type'];
$data['path'] = (string)$data['path'];
$data['dirname'] = (string)$data['dirname'];
$data['filename'] = (string)$data['filename'];
$data['basename'] = (string)$data['basename'];
// optional
$data['extension'] = !empty($data['extension']) ? (string)$data['extension'] : null;
$data['timestamp'] = is_numeric($data['timestamp']) ? (int)$data['timestamp'] : null;
$data['size'] = is_numeric($data['size']) ? (int)$data['size'] : null;
$this->data = $data;
}
/**
* @param array $metainfo
*
* @return PathInfo
*/
public static function fromMeta(array $metainfo = [])
{
$pathinfo = Util::pathinfo($metainfo['path']);
return new PathInfo(array_merge($pathinfo, $metainfo));
}
/**
* @return array
*/
public function toArray()
{
return $this->data;
}
/**
* @return bool
*/
public function isFile()
{
return $this->getType() === self::TYPE_FILE;
}
/**
* @return bool
*/
public function isDir()
{
return $this->getType() === self::TYPE_DIR;
}
/**
* @return string [dir|file]
*/
public function getType()
{
return $this->data['type'];
}
/**
* @return string Filename with extension and without path
*/
public function getBasename()
{
return $this->data['basename'];
}
/**
* @return string Filename without extension and without path
*/
public function getFilename()
{
return $this->data['filename'];
}
/**
* @return string|null File extension or null if directory
*/
public function getExtension()
{
return $this->data['extension'];
}
/**
* @return string Path without filename
*/
public function getDir()
{
return $this->data['dirname'];
}
/**
* Relativer Pfad zum Mountpoint
*
* @return string Path with filename
*/
public function getPath()
{
return $this->data['path'];
}
/**
* @return int|null File size or null if not available
*/
public function getSize()
{
return $this->data['size'];
}
/**
* @return int|null Last updated timestamp or null if not available
*/
public function getTimestamp()
{
return $this->data['timestamp'];
}
/**
* @param string $name
*
* @return bool
*/
public function has($name)
{
return isset($this->data[(string)$name]);
}
/**
* @param string $name
*
* @return mixed
*/
public function get($name)
{
if ($this->has($name)) {
return $this->data[(string)$name];
}
return null;
}
/**
* @param string $name
* @param mixed $value
*
* @throws InvalidArgumentException
*
* @return void
*/
public function set($name, $value)
{
if (in_array((string)$name, self::$readonlyProperties, true)) {
throw new InvalidArgumentException(sprintf('Property "%s" is readonly.', $name));
}
$this->data[(string)$name] = $value;
}
/**
* @param string $name
*
* @return bool
*/
public function __isset($name)
{
return $this->has($name);
}
/**
* @param string $name
*
* @return mixed
*/
public function __get($name)
{
return $this->get($name);
}
/**
* @param string $name
* @param mixed $value
*
* @return void
*/
public function __set($name, $value)
{
$this->set($name, $value);
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Filesystem;
use League\Flysystem\Util;
final class PathUtil extends Util
{
}
@@ -0,0 +1,63 @@
<?php
namespace Xentral\Components\Filesystem\Plugin;
use Xentral\Components\Filesystem\FilesystemInterface;
final class FilterPathsPlugin implements PluginInterface
{
/** @var FilesystemInterface $filesystem */
private $filesystem;
/**
* @return string
*/
public function getMethod()
{
return 'filterPaths';
}
/**
* @param FilesystemInterface $filesystem
*/
public function setFilesystem(FilesystemInterface $filesystem)
{
$this->filesystem = $filesystem;
}
/**
* @example filterPaths(['extension' => 'php', 'filename' => 'Bootstrap'])
*
* @param array $filter
* @param string $path
* @param bool $recursive
*
* @return array
*/
public function handle(array $filter = [], $path = '', $recursive = false)
{
$result = [];
$contents = $this->filesystem->listContents($path, $recursive);
foreach ($contents as $object) {
$matchesFilter = true;
foreach ($filter as $property => $value) {
if (!isset($object[$property])) {
$matchesFilter = false;
continue;
}
if ($object[$property] !== $value) {
$matchesFilter = false;
continue;
}
}
if ($matchesFilter === true) {
$result[] = $object['path'];
}
}
return $result;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Xentral\Components\Filesystem\Plugin;
use League\Flysystem\Plugin\AbstractPlugin;
final class ListDirectoriesPlugin extends AbstractPlugin
{
/**
* Get the method name.
*
* @return string
*/
public function getMethod()
{
return 'listDirectories';
}
/**
* List all directories in the directory.
*
* @param string $directory
* @param bool $recursive
*
* @return array
*/
public function handle($directory = '', $recursive = false)
{
$contents = $this->filesystem->listContents($directory, $recursive);
$filter = function ($object) {
return $object['type'] === 'dir';
};
return array_values(array_filter($contents, $filter));
}
}
@@ -0,0 +1,18 @@
<?php
namespace Xentral\Components\Filesystem\Plugin;
use Xentral\Components\Filesystem\FilesystemInterface;
interface PluginInterface
{
/**
* @return string
*/
public function getMethod();
/**
* @param FilesystemInterface $filesystem
*/
public function setFilesystem(FilesystemInterface $filesystem);
}
+245
View File
@@ -0,0 +1,245 @@
# Filesystem-Komponente
## Beispiele
### FTP
```php
<?php
/** @var \Xentral\Components\Filesystem\FilesystemFactory $factory */
$factory = $this->app->Container->get('FilesystemFactory');
$config = new \Xentral\Components\Filesystem\Adapter\FtpConfig('192.168.0.123', 'username', 'passwort', '/root-dir');
$ftp = $factory->createFtp($config);
if ($ftp->has('/some/file.txt')) {
$contents = $ftp->read('/some/file.txt');
} else {
$ftp->write('/some/file.txt', 'hello world!');
}
```
### Lokales Dateisystem
```php
<?php
/** @var \Xentral\Components\Filesystem\FilesystemFactory $factory */
$factory = $this->app->Container->get('FilesystemFactory');
$local = $factory->createLocal(dirname(__DIR__));
if ($local->has('/some/file.txt')) {
$contents = $local->read('/some/file.txt');
} else {
$local->write('/some/file.txt', 'hello world!');
}
```
### Große Dateien kopieren
```php
<?php
$ftp = $factory->createFtp($config);
$local = $factory->createLocal(dirname(__DIR__));
$ftp->writeStream('/ziel-pfad', $local->readStream('/quell-pfad'));
```
### Datei-Uploads
```php
<?php
$factory = $this->app->Container->get('FilesystemFactory');
$local = $factory->createLocal(dirname(__DIR__));
$stream = fopen($_FILES['upload']['tmp_name'], 'rb+');
$local->writeStream(
'uploads/' . $_FILES['upload']['name'],
$stream
);
if (is_resource($stream)) {
fclose($stream);
}
```
### FilesystemSyncCache
```php
<?php
$ftp = $factory->createFtp($config);
$sync = $factory->createSync($ftp, 1);
$changes = $sync->listChanges('/sub', true); // Hinzugekommene und geänderte Dateien abrufen
$deletes = $sync->listDeleted('/sub', true); // Gelöschte Dateien abrufen
```
## API
### FilesystemInterface
---
#### `has($path)`
Prüfen ob eine Datei oder ein Verzeichnis existiert.
Rückgabe: `true` oder `false`
---
#### `listContents($directory = '', $recursive = false)`
Verzeichnisinhalte abrufen.
Rückgabe: `array` mit `PathInfo`-Objekten
---
#### `listDirs($directory = '', $recursive = false)`
Verzeichnisinhalte abrufen; nur Verzeichnisse.
Rückgabe: `array` mit `PathInfo`-Objekten
---
#### `listFiles($directory = '', $recursive = false)`
Verzeichnisinhalte abrufen; nur Dateien.
Rückgabe: `array` mit `PathInfo`-Objekten
---
#### `listPaths($directory = '', $recursive = false)`
Verzeichnisinhalte abrufen; nur Pfadauflistung; keine Details.
Rückgabe: `array` mit Pfaden als `string`
---
#### `getInfo($path)`
Informationen über eine Datei oder ein Verzeichnis abrufen.
Rückgabe: `PathInfo`-Objekt
---
#### `getType($path)`
Prüfen ob Pfad eine Datei oder ein Verzeichnis ist.
Rückgabe: `'dir'` oder `'file''`
---
#### `getSize($path)`
Dateigröße abrufen.
Rückgabe: Dateigröße als `int` oder `false` bei einem Verzeichnis.
---
#### `getTimestamp($path)`
Datum der letzten Änderung abrufen.
Rückgabe: Timestamp als `int` oder `false` falls Information nicht verfügbar
(z.B. bei Verzeichnissen über FTP).
---
#### `getMimetype($path)`
Mimetype abrufen.
Rückgabe: Mimetype als `string` oder `'directory'` bei einem Verzeichnis.
---
#### `read($path)`
Datei-Inhalt abrufen.
Rückgabe: `string`
---
#### `readStream($path)`
Datei-Inhalt als Stream abrufen.
Rückgabe: `resource`
---
#### `write($path, $contents, array $config = [])`
#### `writeStream($path, $resource, array $config = [])`
Datei anlegen.
Besonderheit: Wenn Zieldatei bereits existiert, wird eine `FileExistsException` geworfen.
Rückgabe: `true` oder `false`
---
#### `put($path, $contents, array $config = [])`
#### `putStream($path, $resource, array $config = [])`
Datei anlegen oder überschreiben.
Besonderheit: Im Unterschied zu `write()` und `writeStream()` wird keine Exception geworfen wenn das Ziel bereits existiert.
Rückgabe: `true` oder `false`
---
#### `createDir($dirname)`
Verzeichnis anlegen; funktioniert auch rekursiv.
Rückgabe: `true` oder `false`
---
#### `deleteDir($dirname)`
Verzeichnis löschen; funktioniert auch wenn Verzeichnis nicht leer.
Rückgabe: `true` oder `false`
---
#### `delete($path)`
Einzelne Datei löschen.
Rückgabe: `true` oder `false`
---
#### `rename($path, $newpath)`
Datei umbenennen.
Rückgabe: `true` oder `false`
---
#### `copy($path, $newpath)`
Datei kopieren.
Rückgabe: `true` oder `false`
---
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Xentral\Components\Http;
use Xentral\Components\Http\Session\SessionHandler;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices()
{
return [
'Request' => 'onInitRequest',
'Session' => 'onInitSession',
];
}
/**
* @return Request
*/
public static function onInitRequest()
{
return Request::createFromGlobals();
}
/**
* @return Session\Session
*/
public static function onInitSession()
{
return SessionHandler::createSession();
}
}
@@ -0,0 +1,223 @@
<?php
namespace Xentral\Components\Http\Collection;
use ArrayIterator;
use Iterator;
use Xentral\Components\Http\File\FileUpload;
class FilesCollection implements Iterator
{
/** @var ArrayIterator $iterator */
protected $iterator;
/**
* @param array $files
*/
public function __construct(array $files = [])
{
$files = $this->loadFilesArray($files);
$this->iterator = new ArrayIterator($files);
}
/**
* Returns all file uploads.
*
* @return FileUpload[]
*/
public function all()
{
return $this->iterator->getArrayCopy();
}
/**
* Returns true if there is a file upload entry with this name.
*
* @param string $name
*
* @return bool
*/
public function has($name)
{
return $this->iterator->offsetExists($name);
}
/**
* Get an file upload entry.
*
* @param string $name
* @param mixed|null $default
*
* @return FileUpload|mixed
*/
public function get($name, $default = null)
{
return $this->has($name) ? $this->iterator->offsetGet($name) : $default;
}
/**
* Returns true if all upload files are valid.
*
* @return bool true=all upload files are valid
*/
public function allValid()
{
foreach ($this->all() as $name => $upload) {
if (!$upload->isValid()) {
return false;
}
}
return true;
}
/**
* Returns true if at least one file upload has an error.
*
* @return bool true=there is at least one error
*/
public function hasErrors()
{
foreach ($this->all() as $name => $upload) {
if ($upload->hasError()) {
return true;
}
}
return false;
}
/**
* Return the current element
*
* @return mixed FileUpload Object, null on failure
*/
public function current()
{
return $this->iterator->current();
}
/**
* Move forward to next element
*
* @return void
*/
public function next()
{
$this->iterator->next();
}
/**
* Return the key of the current element
*
* @return mixed on success, or null on failure.
*/
public function key()
{
return $this->iterator->key();
}
/**
* Checks if current position is valid
*
* @return boolean true on success or false on failure.
*/
public function valid()
{
return $this->iterator->valid();
}
/**
* Rewind the Iterator to the first element
*
* @return void Any returned value is ignored.
*/
public function rewind()
{
$this->iterator->rewind();
}
/**
* @param $fileEntries
*
* @return bool true=is alternate Array
*/
protected function isAlternateArray($fileEntries)
{
if (
!isset($fileEntries[array_keys($fileEntries)[0]]['tmp_name'])
|| is_array($fileEntries[array_keys($fileEntries)[0]]['tmp_name'])
) {
return true;
}
return false;
}
/**
* @param array $fileEntries
*
* @return FileUpload[] $files
*/
protected function loadFilesArray($fileEntries)
{
if (empty($fileEntries)) {
return [];
}
if ($this->isAlternateArray($fileEntries)) {
return $this->convertFilesArray($fileEntries);
}
$files = [];
foreach ($fileEntries as $name => $upload) {
if (empty($upload['tmp_name'])) {
continue;
}
if (!is_array($upload['tmp_name'])) {
$files[$name] = FileUpload::fromFilesArray($upload);
continue;
}
}
return $files;
}
/**
* Recursively builds the array with FileUpload instances from alternative array format.
*
* tested up to 3rd level nesting
*
* @param array $files
*
* @return array
*/
protected function convertFilesArray(array $files)
{
$totalUploads = [];
foreach ($files as $name => $file) {
$keys = array_keys($file);
if (count(array_intersect($keys, ['name', 'tmp_name'])) === 2) {
$uploads = [];
foreach ($file['tmp_name'] as $k => $v) {
$upload = [
'tmp_name' => array_key_exists('tmp_name', $file) ? $file['tmp_name'][$k] : null,
'name' => array_key_exists('name', $file) ? $file['name'][$k] : null,
'type' => array_key_exists('type', $file) ? $file['type'][$k] : null,
'size' => array_key_exists('size', $file) ? $file['size'][$k] : null,
'error' => array_key_exists('error', $file) ? $file['error'][$k] : null,
];
if (!empty($upload['tmp_name'])) {
$uploads[$k] = FileUpload::fromFilesArray($upload);
}
}
$totalUploads[$name] = $uploads;
} else {
if (!(isset($file['tmp_name']) && $file['tmp_name'] === '')) {
$totalUploads[$name] = $this->convertFilesArray($file);
}
}
}
return $totalUploads;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Xentral\Components\Http\Collection;
class ParameterCollection extends ReadonlyParameterCollection
{
/**
* Sets a parameter; Existing parameter value will be overwritten
*
* @param string $name
* @param mixed $value
*/
public function set($name, $value)
{
$this->params[$name] = $value;
}
/**
* Sets multiple parameters; Existing parameters will be overwritten
*
* @param array $values
*/
public function add(array $values)
{
$this->params = array_merge($this->params, $values);
}
/**
* Removes a parameter
*
* @param string $name
*/
public function remove($name)
{
unset($this->params[$name]);
}
}
@@ -0,0 +1,133 @@
<?php
namespace Xentral\Components\Http\Collection;
class ReadonlyParameterCollection
{
/** @var array $params */
protected $params;
/**
* @param array $params
*/
public function __construct(array $params = [])
{
$this->params = [];
foreach ($params as $name => $value) {
$this->params[$name] = $value;
}
}
/**
* Returns all parameters and values as associative array
*
* @return array
*/
public function all()
{
return $this->params;
}
/**
* Returns true if parameter is available
*
* @param string $name
*
* @return bool
*/
public function has($name)
{
return array_key_exists($name, $this->params);
}
/**
* Returns the parameter value
*
* @param string $name
* @param mixed|null $default
*
* @return mixed
*/
public function get($name, $default = null)
{
return isset($this->params[$name]) ? $this->params[$name] : $default;
}
/**
* Returns the parameter value casted to boolean
*
* @param string $name
* @param bool $default
*
* @return bool Returns true for "1", "true", "on" and "yes"; otherwise false
*/
public function getBool($name, $default = false)
{
return filter_var($this->get($name, $default), FILTER_VALIDATE_BOOLEAN);
}
/**
* Returns the parameter value casted to integer
*
* @param string $name
* @param int $default
*
* @return int
*/
public function getInt($name, $default = 0)
{
return (int)$this->get($name, $default);
}
/**
* Returns a string with digits only (0-9)
*
* @param string $name
* @param string $default
*
* @return string
*/
public function getDigits($name, $default = '')
{
return (string)preg_replace('#[^0-9]#', '', $this->get($name, $default));
}
/**
* Returns a string with alphabetic characters only (A-Z and a-z)
*
* @param string $name
* @param string $default
*
* @return string
*/
public function getAlpha($name, $default = '')
{
return (string)preg_replace('#[^A-Za-z]#', '', $this->get($name, $default));
}
/**
* Returns a string with alphanumeric characters only (A-Z, a-z and 0-9)
*
* @param string $name
* @param string $default
*
* @return string
*/
public function getAlphaNum($name, $default = '')
{
return (string)preg_replace('#[^A-Za-z0-9]#', '', $this->get($name, $default));
}
/**
* Returns a string with alphanumeric characters and dashes only (A-Z, a-z, 0-9, Minus and Underscore)
*
* @param string $name
* @param string $default
*
* @return string
*/
public function getAlphaNumWithDashes($name, $default = '')
{
return (string)preg_replace('#[^A-Za-z0-9_-]#', '', $this->get($name, $default));
}
}

Some files were not shown because too many files have changed in this diff Show More